├── .eslintrc.cjs ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── examples ├── express │ ├── README.md │ ├── app.js │ └── package.json ├── next13-approuter-html-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next13-approuter-js-submission-dynamic │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next13-approuter-js-submission-static │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next13-pagesrouter-html-submission │ ├── .eslintrc.json │ ├── README.md │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── form-handler.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── next14-approuter-html-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next14-approuter-js-submission-dynamic │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next14-approuter-js-submission-static │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next14-approuter-sentry │ ├── .eslintrc.json │ ├── .sentryclirc │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── sentry-example-api │ │ │ │ └── route.js │ │ ├── favicon.ico │ │ ├── global-error.jsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── sentry-example-page │ │ │ └── page.jsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── sentry.client.config.ts │ ├── sentry.edge.config.ts │ ├── sentry.server.config.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── next14-approuter-server-action-form-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next14-approuter-server-action-non-form-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── layout.tsx │ │ └── page.tsx │ ├── lib │ │ └── actions.ts │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next14-pagesrouter-html-submission │ ├── .eslintrc.json │ ├── README.md │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── form-handler.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── next15-approuter-html-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next15-approuter-js-submission-dynamic │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next15-approuter-js-submission-static │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── form-handler │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next15-approuter-sentry │ ├── .eslintrc.json │ ├── .sentryclirc │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── sentry-example-api │ │ │ │ └── route.js │ │ ├── favicon.ico │ │ ├── global-error.jsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── sentry-example-page │ │ │ └── page.jsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── sentry.client.config.ts │ ├── sentry.edge.config.ts │ ├── sentry.server.config.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── next15-approuter-server-action-form-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── layout.tsx │ │ └── page.tsx │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next15-approuter-server-action-non-form-submission │ ├── .eslintrc.json │ ├── README.md │ ├── app │ │ ├── layout.tsx │ │ └── page.tsx │ ├── lib │ │ └── actions.ts │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ └── globals.css │ └── tsconfig.json ├── next15-pagesrouter-html-submission │ ├── .eslintrc.json │ ├── README.md │ ├── middleware.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── form-handler.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── node-http │ ├── README.md │ ├── package.json │ └── server.js ├── sveltekit-cloudflare │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── hooks.server.ts │ │ ├── lib │ │ │ └── index.ts │ │ └── routes │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── static │ │ ├── favicon.ico │ │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ ├── vite.config.ts │ └── wrangler.toml └── sveltekit-vercel │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ ├── lib │ │ └── index.ts │ └── routes │ │ ├── +page.server.ts │ │ └── +page.svelte │ ├── static │ ├── favicon.ico │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages ├── core │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── express │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── nextjs │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── node-http │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts └── sveltekit │ ├── README.md │ ├── package.json │ ├── src │ ├── index.test.ts │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── set-version.sh ├── shared ├── package.json ├── src │ ├── config.test.ts │ ├── config.ts │ ├── protect.test.ts │ ├── protect.ts │ ├── util.test.ts │ ├── util.ts │ └── vite-plugin-dts.ts ├── tsconfig.json └── vite.config.ts └── tsconfig.json /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "extends": [ 7 | "airbnb-base", 8 | "airbnb-typescript/base", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "project": "./tsconfig.json" 14 | }, 15 | "ignorePatterns": [], 16 | "rules": { 17 | "@typescript-eslint/naming-convention": "off", 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "import/extensions": "off", 20 | "import/no-extraneous-dependencies": ["error", { 21 | "packageDir": [".", __dirname] // This tells ESLint to check both the local and root package.json files 22 | }], 23 | "import/prefer-default-export": "off", 24 | "max-classes-per-file": "off", 25 | "max-len": "off", 26 | "no-await-in-loop": "off", 27 | "no-restricted-syntax": "off", 28 | "no-underscore-dangle": "off", 29 | "object-curly-newline": "off" 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - uses: pnpm/action-setup@v3 17 | with: 18 | version: 9 19 | - run: pnpm install 20 | - run: pnpm -r lint 21 | 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | - uses: pnpm/action-setup@v3 30 | with: 31 | version: 9 32 | - name: Install dependencies 33 | run: pnpm install 34 | - name: Run tests 35 | run: | 36 | pnpm -r build # only necessary for miniflare 37 | pnpm -r test run -- --environment node 38 | pnpm -r test run -- --environment edge-runtime 39 | pnpm -r test run -- --environment miniflare 40 | 41 | test-nextjs: 42 | runs-on: ubuntu-latest 43 | strategy: 44 | matrix: 45 | version: [13, 14, 15] 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: actions/setup-node@v4 49 | with: 50 | node-version: 20 51 | - uses: pnpm/action-setup@v3 52 | with: 53 | version: 9 54 | - name: Install dependencies 55 | run: pnpm install 56 | - name: Install Next.js version 57 | working-directory: packages/nextjs 58 | run: pnpm install next@${{ matrix.version }} 59 | - name: Run tests 60 | working-directory: packages/nextjs 61 | run: | 62 | pnpm build # only necessary for miniflare 63 | pnpm test run -- --environment node 64 | pnpm test run -- --environment edge-runtime 65 | pnpm test run -- --environment miniflare 66 | 67 | build: 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | - uses: actions/setup-node@v4 72 | with: 73 | node-version: 20 74 | - uses: pnpm/action-setup@v3 75 | with: 76 | version: 9 77 | - run: pnpm install 78 | - run: pnpm -r build 79 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | packages: write 13 | contents: read 14 | steps: 15 | - name: Get tag name 16 | uses: olegtarasov/get-tag@2.1.3 17 | id: tagName 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | registry-url: https://registry.npmjs.org 23 | - uses: pnpm/action-setup@v3 24 | with: 25 | version: 8 26 | - run: pnpm install 27 | - run: pnpm -r build 28 | - run: /bin/bash set-version.sh ${{ steps.tagName.outputs.tag }} 29 | - run: pnpm -r publish --no-git-checks --access public 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git-ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | .DS_Store 7 | *~ 8 | \#* 9 | node_modules 10 | .next 11 | .env 12 | .env*.local 13 | .env.* 14 | !.env.example 15 | dist 16 | build 17 | .vscode 18 | 19 | # vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | 23 | # sveltkit 24 | .svelte-kit 25 | 26 | # vercel 27 | .vercel 28 | 29 | # wrangler files 30 | .wrangler 31 | .dev.vars 32 | 33 | # lerna 34 | .nx 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Andres Morey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | ALL | :white_check_mark: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you find a security vulnerability please send us an email 15 | (hello@kubetail.com) and we will respond to you ASAP. 16 | -------------------------------------------------------------------------------- /examples/express/README.md: -------------------------------------------------------------------------------- 1 | This is an [Express](https://expressjs.com) example app. 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the server: 16 | 17 | ```bash 18 | node app.js 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | -------------------------------------------------------------------------------- /examples/express/app.js: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/express'; 2 | import express from 'express'; 3 | 4 | // initalize csrf protection middleware 5 | const csrfMiddleware = createCsrfMiddleware({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | // init app 12 | const app = express(); 13 | const port = 3000; 14 | 15 | // add csrf middleware 16 | app.use(csrfMiddleware); 17 | 18 | // define handlers 19 | app.get('/', (req, res) => { 20 | const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; 21 | res.send(` 22 | 23 | 24 | 25 |

CSRF token value: ${csrfToken}

26 |

HTML Form Submission Example:

27 |
28 | Form without CSRF (should fail): 29 | 30 | 31 |
32 |
33 |
34 | Form with incorrect CSRF (should fail): 35 | 36 | 37 | 38 |
39 |
40 |
41 | Form with CSRF (should succeed): 42 | 43 | 44 | 45 |
46 | 47 | 48 | `); 49 | }); 50 | 51 | app.post('/form-handler', (req, res) => { 52 | res.send('success'); 53 | }); 54 | 55 | // start server 56 | app.listen(port, () => { 57 | console.log(`Example app listening on port ${port}`) 58 | }); 59 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@edge-csrf/express": "^2.2.0", 8 | "express": "^4.19.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf html form submission example', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | 3 | import '../styles/globals.css'; 4 | 5 | export default function Page() { 6 | const csrfToken = headers().get('X-CSRF-Token') || 'missing'; 7 | 8 | return ( 9 | <> 10 |

11 | CSRF token value: 12 | {csrfToken} 13 |

14 |

HTML Form Submission Example:

15 |
16 | Form without CSRF (should fail): 17 | 18 | 19 |
20 |
21 |
22 | Form with incorrect CSRF (should fail): 23 | 24 | 25 | 26 |
27 |
28 |
29 | Form with CSRF (should succeed): 30 | 31 | 32 | 33 |
34 |

HTML File Upload Example:

35 |
36 | Form without CSRF (should fail): 37 | 38 | 39 |
40 |
41 |
42 | Form with incorrect CSRF (should fail): 43 | 44 | 45 | 46 |
47 |
48 |
49 | Form with CSRF (should succeed): 50 | 51 | 52 | 53 |
54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^13.5.6", 18 | "next": "13.5.6", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next13-approuter-html-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next13-approuter-html-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { headers } from 'next/headers'; 3 | 4 | export async function generateMetadata(): Promise { 5 | const csrfToken = headers().get('X-CSRF-Token') || 'missing'; 6 | 7 | return { 8 | title: 'edge-csrf example', 9 | other: { 10 | 'x-csrf-token': csrfToken, 11 | }, 12 | }; 13 | } 14 | 15 | export default function Layout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | 5 | import '../styles/globals.css'; 6 | 7 | export default function Page() { 8 | const [csrfToken, setCsrfToken] = useState('loading...'); 9 | 10 | useEffect(() => { 11 | const el = document.querySelector('meta[name="x-csrf-token"]') as HTMLMetaElement | null; 12 | if (el) setCsrfToken(el.content); 13 | else setCsrfToken('missing'); 14 | }, []); 15 | 16 | // method to generate form handlers 17 | const onSubmit = (tokenVal: string | null): React.FormEventHandler => ( 18 | async (event: React.FormEvent) => { 19 | event.preventDefault(); 20 | 21 | // get form values 22 | const data = new FormData(event.currentTarget); 23 | 24 | // build fetch args 25 | const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) }; 26 | if (tokenVal != null) fetchArgs.headers = { 'X-CSRF-Token': tokenVal }; 27 | 28 | // send to backend 29 | const response = await fetch('/form-handler', fetchArgs); 30 | 31 | // show response 32 | // eslint-disable-next-line no-alert 33 | alert(response.statusText); 34 | } 35 | ); 36 | 37 | return ( 38 | <> 39 |

JavaScript Form Submission Example:

40 |

41 | CSRF token value: 42 | {csrfToken} 43 |

44 |
45 | Form without CSRF (should fail): 46 | 47 | 48 |
49 |
50 |
51 | Form with incorrect CSRF (should fail): 52 | 53 | 54 |
55 |
56 |
57 | Form with CSRF (should succeed): 58 | 59 | 60 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^13.5.6", 18 | "next": "^13.5.6", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next13-approuter-js-submission-dynamic/public/favicon.ico -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-dynamic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import '../styles/globals.css'; 4 | 5 | export default function Page() { 6 | const handleSubmit = async (ev: React.FormEvent) => { 7 | // prevent default form submission 8 | ev.preventDefault(); 9 | 10 | // get form values 11 | const data = new FormData(ev.currentTarget); 12 | 13 | // get token (see middleware.ts) 14 | const csrfResp = await fetch('/csrf-token'); 15 | const { csrfToken } = await csrfResp.json(); 16 | 17 | // build fetch args 18 | const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) }; 19 | if (csrfToken) fetchArgs.headers = { 'X-CSRF-Token': csrfToken }; 20 | 21 | // send to backend 22 | const response = await fetch('/form-handler', fetchArgs); 23 | 24 | // show response 25 | // eslint-disable-next-line no-alert 26 | alert(response.statusText); 27 | }; 28 | 29 | return ( 30 | <> 31 |

JavaScript Form Submission Example (Static Optimized):

32 |
33 | Form fetches CSRF token before submission (should succeed): 34 | 35 | 36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^13.5.6", 18 | "next": "^13.5.6", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next13-approuter-js-submission-static/public/favicon.ico -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next13-approuter-js-submission-static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/function-component-definition": "off", 5 | "react/jsx-props-no-spreading": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^13.5.6", 18 | "next": "^13.5.6", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/pages/api/form-handler.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | type Data = { 4 | status: string; 5 | }; 6 | 7 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 8 | res.status(200).json({ status: 'success' }); 9 | } 10 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage, GetServerSideProps } from 'next'; 2 | 3 | type Props = { 4 | csrfToken: string; 5 | }; 6 | 7 | export const getServerSideProps: GetServerSideProps = async ({ res }) => { 8 | const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; 9 | return { props: { csrfToken } }; 10 | }; 11 | 12 | const Home: NextPage = ({ csrfToken }) => ( 13 | <> 14 |

15 | CSRF token value: 16 | {csrfToken} 17 |

18 |

HTML Form Submission Example

19 |
20 | Form without CSRF (should fail): 21 | 22 | 23 |
24 |
25 |
26 | Form with incorrect CSRF (should fail): 27 | 28 | 29 | 30 |
31 |
32 |
33 | Form with CSRF (should succeed): 34 | 35 | 36 | 37 |
38 | 39 | ); 40 | 41 | export default Home; 42 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next13-pagesrouter-html-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next13-pagesrouter-html-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf html form submission example', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | 3 | import '../styles/globals.css'; 4 | 5 | export default function Page() { 6 | const csrfToken = headers().get('X-CSRF-Token') || 'missing'; 7 | 8 | return ( 9 | <> 10 |

11 | CSRF token value: 12 | {csrfToken} 13 |

14 |

HTML Form Submission Example:

15 |
16 | Form without CSRF (should fail): 17 | 18 | 19 |
20 |
21 |
22 | Form with incorrect CSRF (should fail): 23 | 24 | 25 | 26 |
27 |
28 |
29 | Form with CSRF (should succeed): 30 | 31 | 32 | 33 |
34 |

HTML File Upload Example:

35 |
36 | Form without CSRF (should fail): 37 | 38 | 39 |
40 |
41 |
42 | Form with incorrect CSRF (should fail): 43 | 44 | 45 | 46 |
47 |
48 |
49 | Form with CSRF (should succeed): 50 | 51 | 52 | 53 |
54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^14.0.1", 18 | "next": "^14.0.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-approuter-html-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-html-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ] 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { headers } from 'next/headers'; 3 | 4 | export async function generateMetadata(): Promise { 5 | const csrfToken = headers().get('X-CSRF-Token') || 'missing'; 6 | 7 | return { 8 | title: 'edge-csrf example', 9 | other: { 10 | 'x-csrf-token': csrfToken, 11 | }, 12 | }; 13 | } 14 | 15 | export default function Layout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | 5 | import '../styles/globals.css'; 6 | 7 | export default function Page() { 8 | const [csrfToken, setCsrfToken] = useState('loading...'); 9 | 10 | useEffect(() => { 11 | const el = document.querySelector('meta[name="x-csrf-token"]') as HTMLMetaElement | null; 12 | if (el) setCsrfToken(el.content); 13 | else setCsrfToken('missing'); 14 | }, []); 15 | 16 | // method to generate form handlers 17 | const onSubmit = (tokenVal: string | null): React.FormEventHandler => ( 18 | async (event: React.FormEvent) => { 19 | event.preventDefault(); 20 | 21 | // get form values 22 | const data = new FormData(event.currentTarget); 23 | 24 | // build fetch args 25 | const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) }; 26 | if (tokenVal != null) fetchArgs.headers = { 'X-CSRF-Token': tokenVal }; 27 | 28 | // send to backend 29 | const response = await fetch('/form-handler', fetchArgs); 30 | 31 | // show response 32 | // eslint-disable-next-line no-alert 33 | alert(response.statusText); 34 | } 35 | ); 36 | 37 | return ( 38 | <> 39 |

JavaScript Form Submission Example:

40 |

41 | CSRF token value: 42 | {csrfToken} 43 |

44 |
45 | Form without CSRF (should fail): 46 | 47 | 48 |
49 |
50 |
51 | Form with incorrect CSRF (should fail): 52 | 53 | 54 |
55 |
56 |
57 | Form with CSRF (should succeed): 58 | 59 | 60 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^14.0.1", 18 | "next": "^14.0.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-approuter-js-submission-dynamic/public/favicon.ico -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-dynamic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import '../styles/globals.css'; 4 | 5 | export default function Page() { 6 | const handleSubmit = async (ev: React.FormEvent) => { 7 | // prevent default form submission 8 | ev.preventDefault(); 9 | 10 | // get form values 11 | const data = new FormData(ev.currentTarget); 12 | 13 | // get token (see middleware.ts) 14 | const csrfResp = await fetch('/csrf-token'); 15 | const { csrfToken } = await csrfResp.json(); 16 | 17 | // build fetch args 18 | const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) }; 19 | if (csrfToken) fetchArgs.headers = { 'X-CSRF-Token': csrfToken }; 20 | 21 | // send to backend 22 | const response = await fetch('/form-handler', fetchArgs); 23 | 24 | // show response 25 | // eslint-disable-next-line no-alert 26 | alert(response.statusText); 27 | }; 28 | 29 | return ( 30 | <> 31 |

JavaScript Form Submission Example (Static Optimized):

32 |
33 | Form fetches CSRF token before submission (should succeed): 34 | 35 | 36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^14.0.1", 18 | "next": "^14.0.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-approuter-js-submission-static/public/favicon.ico -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-js-submission-static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/.sentryclirc: -------------------------------------------------------------------------------- 1 | 2 | [auth] 3 | token=REPLACEME 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/app/api/sentry-example-api/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | // A faulty API route to test Sentry's error monitoring 6 | export function GET() { 7 | throw new Error("Sentry Example API Route Error"); 8 | return NextResponse.json({ data: "Testing Sentry Error..." }); 9 | } 10 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-approuter-sentry/app/favicon.ico -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/app/global-error.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import Error from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | export default function GlobalError({ error }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | 6 | 7 | // Injected content via Sentry wizard below 8 | 9 | const { withSentryConfig } = require("@sentry/nextjs"); 10 | 11 | module.exports = withSentryConfig( 12 | module.exports, 13 | { 14 | // For all available options, see: 15 | // https://github.com/getsentry/sentry-webpack-plugin#options 16 | 17 | // Suppresses source map uploading logs during build 18 | silent: true, 19 | org: "REPLACEME", 20 | project: "REPLACEME", 21 | }, 22 | { 23 | // For all available options, see: 24 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 25 | 26 | // Upload a larger set of source maps for prettier stack traces (increases build time) 27 | widenClientFileUpload: true, 28 | 29 | // Transpiles SDK to be compatible with IE11 (increases bundle size) 30 | transpileClientSDK: true, 31 | 32 | // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) 33 | tunnelRoute: "/monitoring", 34 | 35 | // Hides source maps from generated client bundles 36 | hideSourceMaps: true, 37 | 38 | // Automatically tree-shake Sentry logger statements to reduce bundle size 39 | disableLogger: true, 40 | 41 | // Enables automatic instrumentation of Vercel Cron Monitors. 42 | // See the following for more information: 43 | // https://docs.sentry.io/product/crons/ 44 | // https://vercel.com/docs/cron-jobs 45 | automaticVercelMonitors: true, 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@sentry/nextjs": "^7.93.0", 14 | "next": "14.2.1", 15 | "react": "^18", 16 | "react-dom": "^18" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20", 20 | "@types/react": "^18", 21 | "@types/react-dom": "^18", 22 | "autoprefixer": "^10.0.1", 23 | "eslint": "^8", 24 | "eslint-config-next": "14.0.4", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.3.0", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | import type { BaseTransportOptions } from '@sentry/types'; 7 | 8 | async function fetchWithCSRFHeader(input: RequestInfo | URL, init: RequestInit = {}): Promise { 9 | // get csrf token (see middleware.ts) 10 | const csrfResp = await fetch('/csrf-token'); 11 | const { csrfToken } = await csrfResp.json(); 12 | 13 | // add token to headers 14 | const headers = new Headers(init.headers); 15 | headers.append('X-CSRF-Token', csrfToken); 16 | 17 | // construct init object with the updated headers 18 | const modifiedInit = { ...init, headers }; 19 | 20 | // call native fetch function with the original input and the modified init object 21 | return fetch(input, modifiedInit); 22 | } 23 | 24 | Sentry.init({ 25 | dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME", 26 | 27 | // Adjust this value in production, or use tracesSampler for greater control 28 | tracesSampleRate: 1, 29 | 30 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 31 | debug: false, 32 | 33 | replaysOnErrorSampleRate: 1.0, 34 | 35 | // This sets the sample rate to be 10%. You may want this to be 100% while 36 | // in development and sample at a lower rate in production 37 | replaysSessionSampleRate: 0.1, 38 | 39 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 40 | integrations: [ 41 | new Sentry.Replay({ 42 | // Additional Replay configuration goes in here, for example: 43 | maskAllText: true, 44 | blockAllMedia: true, 45 | }), 46 | ], 47 | 48 | transport: (options: BaseTransportOptions) => { 49 | return Sentry.makeFetchTransport(options, fetchWithCSRFHeader); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME", 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /examples/next14-approuter-sentry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^14.0.1", 18 | "next": "^14.2.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-approuter-server-action-form-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-form-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { example1, example2 } from '../lib/actions'; 4 | 5 | export default function Page() { 6 | const handleClick1 = async () => { 7 | const csrfResp = await fetch('/csrf-token'); 8 | const { csrfToken } = await csrfResp.json(); 9 | 10 | const data = { 11 | key1: 'val1', 12 | key2: 'val2', 13 | }; 14 | 15 | // use token as first argument to server action 16 | await example1(csrfToken, data); 17 | }; 18 | 19 | const handleClick2 = async () => { 20 | const csrfResp = await fetch('/csrf-token'); 21 | const { csrfToken } = await csrfResp.json(); 22 | 23 | // add token to FormData instance 24 | const data = new FormData(); 25 | data.set('csrf_token', csrfToken); 26 | data.set('key1', 'val1'); 27 | data.set('key2', 'val2'); 28 | 29 | await example2(data); 30 | }; 31 | 32 | return ( 33 | <> 34 |

Server Action Non-Form Submission Examples:

35 |

NOTE: Look at browser network logs and server console for submission feedback

36 |

Example with object argument:

37 | 38 |

Example with FormData argument:

39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/lib/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | export async function example1(csrfToken: string, data: { key1: string; key2: string; }) { 4 | // eslint-disable-next-line no-console 5 | console.log(data); 6 | } 7 | 8 | export async function example2(data: FormData) { 9 | // eslint-disable-next-line no-console 10 | console.log(data); 11 | } 12 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^14.0.1", 18 | "next": "^14.0.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-approuter-server-action-non-form-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next14-approuter-server-action-non-form-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/function-component-definition": "off", 5 | "react/jsx-props-no-spreading": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true 5 | } 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^14.0.1", 18 | "next": "^14.2.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/pages/api/form-handler.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | type Data = { 4 | status: string; 5 | }; 6 | 7 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 8 | res.status(200).json({ status: 'success' }); 9 | } 10 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage, GetServerSideProps } from 'next'; 2 | 3 | type Props = { 4 | csrfToken: string; 5 | }; 6 | 7 | export const getServerSideProps: GetServerSideProps = async ({ res }) => { 8 | const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; 9 | return { props: { csrfToken } }; 10 | }; 11 | 12 | const Home: NextPage = ({ csrfToken }) => ( 13 | <> 14 |

15 | CSRF token value: 16 | {csrfToken} 17 |

18 |

HTML Form Submission Example

19 |
20 | Form without CSRF (should fail): 21 | 22 | 23 |
24 |
25 |
26 | Form with incorrect CSRF (should fail): 27 | 28 | 29 | 30 |
31 |
32 |
33 | Form with CSRF (should succeed): 34 | 35 | 36 | 37 |
38 | 39 | ); 40 | 41 | export default Home; 42 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next14-pagesrouter-html-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next14-pagesrouter-html-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf html form submission example', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^15.0.0", 18 | "next": "^15.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-approuter-html-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-html-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "target": "ES2017", 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { headers } from 'next/headers'; 3 | 4 | export async function generateMetadata(): Promise { 5 | const headersList = await headers(); 6 | const csrfToken = headersList.get('X-CSRF-Token') || 'missing'; 7 | 8 | return { 9 | title: 'edge-csrf example', 10 | other: { 11 | 'x-csrf-token': csrfToken, 12 | }, 13 | }; 14 | } 15 | 16 | export default function Layout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | {children} 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | 5 | import '../styles/globals.css'; 6 | 7 | export default function Page() { 8 | const [csrfToken, setCsrfToken] = useState('loading...'); 9 | 10 | useEffect(() => { 11 | const el = document.querySelector('meta[name="x-csrf-token"]') as HTMLMetaElement | null; 12 | if (el) setCsrfToken(el.content); 13 | else setCsrfToken('missing'); 14 | }, []); 15 | 16 | // method to generate form handlers 17 | const onSubmit = (tokenVal: string | null): React.FormEventHandler => ( 18 | async (event: React.FormEvent) => { 19 | event.preventDefault(); 20 | 21 | // get form values 22 | const data = new FormData(event.currentTarget); 23 | 24 | // build fetch args 25 | const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) }; 26 | if (tokenVal != null) fetchArgs.headers = { 'X-CSRF-Token': tokenVal }; 27 | 28 | // send to backend 29 | const response = await fetch('/form-handler', fetchArgs); 30 | 31 | // show response 32 | // eslint-disable-next-line no-alert 33 | alert(response.statusText); 34 | } 35 | ); 36 | 37 | return ( 38 | <> 39 |

JavaScript Form Submission Example:

40 |

41 | CSRF token value: 42 | {csrfToken} 43 |

44 |
45 | Form without CSRF (should fail): 46 | 47 | 48 |
49 |
50 |
51 | Form with incorrect CSRF (should fail): 52 | 53 | 54 |
55 |
56 |
57 | Form with CSRF (should succeed): 58 | 59 | 60 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^15.0.0", 18 | "next": "^15.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-approuter-js-submission-dynamic/public/favicon.ico -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-dynamic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/app/form-handler/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function POST() { 4 | return NextResponse.json({ status: 'success' }); 5 | } 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import '../styles/globals.css'; 4 | 5 | export default function Page() { 6 | const handleSubmit = async (ev: React.FormEvent) => { 7 | // prevent default form submission 8 | ev.preventDefault(); 9 | 10 | // get form values 11 | const data = new FormData(ev.currentTarget); 12 | 13 | // get token (see middleware.ts) 14 | const csrfResp = await fetch('/csrf-token'); 15 | const { csrfToken } = await csrfResp.json(); 16 | 17 | // build fetch args 18 | const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) }; 19 | if (csrfToken) fetchArgs.headers = { 'X-CSRF-Token': csrfToken }; 20 | 21 | // send to backend 22 | const response = await fetch('/form-handler', fetchArgs); 23 | 24 | // show response 25 | // eslint-disable-next-line no-alert 26 | alert(response.statusText); 27 | }; 28 | 29 | return ( 30 | <> 31 |

JavaScript Form Submission Example (Static Optimized):

32 |
33 | Form fetches CSRF token before submission (should succeed): 34 | 35 | 36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^15.0.0", 18 | "next": "^15.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-approuter-js-submission-static/public/favicon.ico -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-js-submission-static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/.sentryclirc: -------------------------------------------------------------------------------- 1 | 2 | [auth] 3 | token=REPLACEME 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/app/api/sentry-example-api/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export const dynamic = "force-dynamic"; 4 | 5 | // A faulty API route to test Sentry's error monitoring 6 | export function GET() { 7 | throw new Error("Sentry Example API Route Error"); 8 | return NextResponse.json({ data: "Testing Sentry Error..." }); 9 | } 10 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-approuter-sentry/app/favicon.ico -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/app/global-error.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import Error from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | export default function GlobalError({ error }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | 6 | 7 | // Injected content via Sentry wizard below 8 | 9 | const { withSentryConfig } = require("@sentry/nextjs"); 10 | 11 | module.exports = withSentryConfig( 12 | module.exports, 13 | { 14 | // For all available options, see: 15 | // https://github.com/getsentry/sentry-webpack-plugin#options 16 | 17 | // Suppresses source map uploading logs during build 18 | silent: true, 19 | org: "REPLACEME", 20 | project: "REPLACEME", 21 | }, 22 | { 23 | // For all available options, see: 24 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 25 | 26 | // Upload a larger set of source maps for prettier stack traces (increases build time) 27 | widenClientFileUpload: true, 28 | 29 | // Transpiles SDK to be compatible with IE11 (increases bundle size) 30 | transpileClientSDK: true, 31 | 32 | // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) 33 | tunnelRoute: "/monitoring", 34 | 35 | // Hides source maps from generated client bundles 36 | hideSourceMaps: true, 37 | 38 | // Automatically tree-shake Sentry logger statements to reduce bundle size 39 | disableLogger: true, 40 | 41 | // Enables automatic instrumentation of Vercel Cron Monitors. 42 | // See the following for more information: 43 | // https://docs.sentry.io/product/crons/ 44 | // https://vercel.com/docs/cron-jobs 45 | automaticVercelMonitors: true, 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@sentry/nextjs": "^7.93.0", 14 | "next": "^15.0.0", 15 | "react": "^18", 16 | "react-dom": "^18" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20", 20 | "@types/react": "^18", 21 | "@types/react-dom": "^18", 22 | "autoprefixer": "^10.0.1", 23 | "eslint": "^8", 24 | "eslint-config-next": "^15.0.0", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.3.0", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | import type { BaseTransportOptions } from '@sentry/types'; 7 | 8 | async function fetchWithCSRFHeader(input: RequestInfo | URL, init: RequestInit = {}): Promise { 9 | // get csrf token (see middleware.ts) 10 | const csrfResp = await fetch('/csrf-token'); 11 | const { csrfToken } = await csrfResp.json(); 12 | 13 | // add token to headers 14 | const headers = new Headers(init.headers); 15 | headers.append('X-CSRF-Token', csrfToken); 16 | 17 | // construct init object with the updated headers 18 | const modifiedInit = { ...init, headers }; 19 | 20 | // call native fetch function with the original input and the modified init object 21 | return fetch(input, modifiedInit); 22 | } 23 | 24 | Sentry.init({ 25 | dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME", 26 | 27 | // Adjust this value in production, or use tracesSampler for greater control 28 | tracesSampleRate: 1, 29 | 30 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 31 | debug: false, 32 | 33 | replaysOnErrorSampleRate: 1.0, 34 | 35 | // This sets the sample rate to be 10%. You may want this to be 100% while 36 | // in development and sample at a lower rate in production 37 | replaysSessionSampleRate: 0.1, 38 | 39 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 40 | integrations: [ 41 | new Sentry.Replay({ 42 | // Additional Replay configuration goes in here, for example: 43 | maskAllText: true, 44 | blockAllMedia: true, 45 | }), 46 | ], 47 | 48 | transport: (options: BaseTransportOptions) => { 49 | return Sentry.makeFetchTransport(options, fetchWithCSRFHeader); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME", 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /examples/next15-approuter-sentry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^15.0.0", 18 | "next": "^15.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-approuter-server-action-form-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-form-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'edge-csrf examples', 5 | }; 6 | 7 | export default function Layout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { example1, example2 } from '../lib/actions'; 4 | 5 | export default function Page() { 6 | const handleClick1 = async () => { 7 | const csrfResp = await fetch('/csrf-token'); 8 | const { csrfToken } = await csrfResp.json(); 9 | 10 | const data = { 11 | key1: 'val1', 12 | key2: 'val2', 13 | }; 14 | 15 | // use token as first argument to server action 16 | await example1(csrfToken, data); 17 | }; 18 | 19 | const handleClick2 = async () => { 20 | const csrfResp = await fetch('/csrf-token'); 21 | const { csrfToken } = await csrfResp.json(); 22 | 23 | // add token to FormData instance 24 | const data = new FormData(); 25 | data.set('csrf_token', csrfToken); 26 | data.set('key1', 'val1'); 27 | data.set('key2', 'val2'); 28 | 29 | await example2(data); 30 | }; 31 | 32 | return ( 33 | <> 34 |

Server Action Non-Form Submission Examples:

35 |

NOTE: Look at browser network logs and server console for submission feedback

36 |

Example with object argument:

37 | 38 |

Example with FormData argument:

39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/lib/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | export async function example1(csrfToken: string, data: { key1: string; key2: string; }) { 4 | // eslint-disable-next-line no-console 5 | console.log(data); 6 | } 7 | 8 | export async function example2(data: FormData) { 9 | // eslint-disable-next-line no-console 10 | console.log(data); 11 | } 12 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | import type { NextRequest } from 'next/server'; 4 | 5 | const csrfProtect = createCsrfProtect({ 6 | cookie: { 7 | secure: process.env.NODE_ENV === 'production', 8 | }, 9 | }); 10 | 11 | export async function middleware(request: NextRequest) { 12 | const response = NextResponse.next(); 13 | 14 | // csrf protection 15 | try { 16 | await csrfProtect(request, response); 17 | } catch (err) { 18 | if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); 19 | throw err; 20 | } 21 | 22 | // return token (for use in static-optimized-example) 23 | if (request.nextUrl.pathname === '/csrf-token') { 24 | return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' }); 25 | } 26 | 27 | return response; 28 | } 29 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^15.0.0", 18 | "next": "^15.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-approuter-server-action-non-form-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: blue; 11 | text-decoration: underline; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next15-approuter-server-action-non-form-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/function-component-definition": "off", 5 | "react/jsx-props-no-spreading": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | pnpm dev 21 | # or 22 | yarn dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfMiddleware } from '@edge-csrf/nextjs'; 2 | 3 | // initalize csrf protection middleware 4 | const csrfMiddleware = createCsrfMiddleware({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const middleware = csrfMiddleware; 11 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@edge-csrf/nextjs": "^2.0.0", 13 | "@types/node": "^20.8.9", 14 | "@types/react": "^18.2.33", 15 | "@types/react-dom": "^18.2.14", 16 | "eslint": "^8.52.0", 17 | "eslint-config-next": "^15.0.0", 18 | "next": "^15.0.0", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/pages/api/form-handler.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | type Data = { 4 | status: string; 5 | }; 6 | 7 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 8 | res.status(200).json({ status: 'success' }); 9 | } 10 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage, GetServerSideProps } from 'next'; 2 | 3 | type Props = { 4 | csrfToken: string; 5 | }; 6 | 7 | export const getServerSideProps: GetServerSideProps = async ({ res }) => { 8 | const csrfToken = res.getHeader('X-CSRF-Token') || 'missing'; 9 | return { props: { csrfToken } }; 10 | }; 11 | 12 | const Home: NextPage = ({ csrfToken }) => ( 13 | <> 14 |

15 | CSRF token value: 16 | {csrfToken} 17 |

18 |

HTML Form Submission Example

19 |
20 | Form without CSRF (should fail): 21 | 22 | 23 |
24 |
25 |
26 | Form with incorrect CSRF (should fail): 27 | 28 | 29 | 30 |
31 |
32 |
33 | Form with CSRF (should succeed): 34 | 35 | 36 | 37 |
38 | 39 | ); 40 | 41 | export default Home; 42 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/next15-pagesrouter-html-submission/public/favicon.ico -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/next15-pagesrouter-html-submission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/node-http/README.md: -------------------------------------------------------------------------------- 1 | This is an [Express](https://expressjs.com) example app. 2 | 3 | ## Getting Started 4 | 5 | First, install dependencies: 6 | 7 | ```bash 8 | npm install 9 | # or 10 | pnpm install 11 | # or 12 | yarn install 13 | ``` 14 | 15 | Next, run the server: 16 | 17 | ```bash 18 | node server.js 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 22 | -------------------------------------------------------------------------------- /examples/node-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edge-csrf-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "dependencies": { 7 | "@edge-csrf/node-http": "^2.2.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | 12 | # wrangler files 13 | .wrangler 14 | .dev.vars 15 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-cloudflare", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "pnpm run build && wrangler pages dev .svelte-kit/cloudflare", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "deploy": "pnpm run build && wrangler pages deploy .svelte-kit/cloudflare", 12 | "build-cf-types": "wrangler types && mv worker-configuration.d.ts src/" 13 | }, 14 | "devDependencies": { 15 | "@cloudflare/workers-types": "^4.20240405.0", 16 | "@sveltejs/adapter-auto": "^3.0.0", 17 | "@sveltejs/adapter-cloudflare": "^4.3.0", 18 | "@sveltejs/kit": "^2.0.0", 19 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 20 | "svelte": "^4.2.7", 21 | "svelte-check": "^3.6.0", 22 | "tslib": "^2.4.1", 23 | "typescript": "^5.0.0", 24 | "vite": "^5.0.3", 25 | "wrangler": "^3.50.0" 26 | }, 27 | "type": "module", 28 | "dependencies": { 29 | "@edge-csrf/sveltekit": "^2.0.0" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import type { CsrfLocals } from '@edge-csrf/sveltekit'; 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | declare global { 6 | namespace App { 7 | interface Locals extends CsrfLocals {} 8 | 9 | interface Platform { 10 | env: Env 11 | cf: CfProperties 12 | ctx: ExecutionContext 13 | } 14 | } 15 | } 16 | 17 | export {}; 18 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfHandle } from '@edge-csrf/sveltekit'; 2 | 3 | // initalize csrf protection handle 4 | const csrfHandle = createCsrfHandle({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const handle = csrfHandle; 11 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | export async function load({ locals }) { 2 | return { 3 | csrfToken: locals.csrfToken, 4 | }; 5 | } 6 | 7 | export const actions = { 8 | default: async () => { 9 | return { success: true }; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if form?.success} 8 |

success

9 | {:else} 10 |

CSRF token value: {data.csrfToken}

11 |

HTML Form Submission Example:

12 |
13 | Form without CSRF (should fail): 14 | 15 | 16 |
17 |
18 |
19 | Form with incorrect CSRF (should fail): 20 | 21 | 22 | 23 |
24 |
25 |
26 | Form with CSRF (should succeed): 27 | 28 | 29 | 30 |
31 | {/if} 32 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/sveltekit-cloudflare/static/favicon.ico -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/sveltekit-cloudflare/static/favicon.png -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-cloudflare"; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "types": [ 14 | "@cloudflare/workers-types/2023-07-01" 15 | ] 16 | } 17 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 18 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 19 | // 20 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 21 | // from the referenced tsconfig.json - TypeScript does not merge them in 22 | } 23 | -------------------------------------------------------------------------------- /examples/sveltekit-cloudflare/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | .vercel 12 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import ts from 'typescript-eslint'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | 6 | /** @type {import('eslint').Linter.FlatConfig[]} */ 7 | export default [ 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | { 12 | languageOptions: { 13 | globals: { 14 | ...globals.browser, 15 | ...globals.node 16 | } 17 | } 18 | }, 19 | { 20 | files: ['**/*.svelte'], 21 | languageOptions: { 22 | parserOptions: { 23 | parser: ts.parser 24 | } 25 | } 26 | }, 27 | { 28 | ignores: ['build/', '.svelte-kit/', 'package/'] 29 | } 30 | ]; 31 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-vercel", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "eslint ." 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-auto": "^3.0.0", 15 | "@sveltejs/adapter-vercel": "^5.2.0", 16 | "@sveltejs/kit": "^2.0.0", 17 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 18 | "@types/eslint": "^8.56.7", 19 | "@types/node": "^20.12.7", 20 | "eslint": "^8.57.0", 21 | "eslint-plugin-svelte": "^2.36.0", 22 | "globals": "^15.0.0", 23 | "svelte": "^4.2.7", 24 | "svelte-check": "^3.6.0", 25 | "tslib": "^2.4.1", 26 | "typescript": "^5.0.0", 27 | "typescript-eslint": "^7.5.0", 28 | "vercel": "^34.0.0", 29 | "vite": "^5.0.3" 30 | }, 31 | "type": "module", 32 | "dependencies": { 33 | "@edge-csrf/sveltekit": "^2.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import type { CsrfLocals } from '@edge-csrf/sveltekit'; 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | declare global { 6 | namespace App { 7 | // interface Error {} 8 | interface Locals extends CsrfLocals {} 9 | // interface PageData {} 10 | // interface PageState {} 11 | // interface Platform {} 12 | } 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { createCsrfHandle } from '@edge-csrf/sveltekit'; 2 | 3 | // initalize csrf protection handle 4 | const csrfHandle = createCsrfHandle({ 5 | cookie: { 6 | secure: process.env.NODE_ENV === 'production', 7 | }, 8 | }); 9 | 10 | export const handle = csrfHandle; 11 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | export async function load({ locals }) { 2 | return { 3 | csrfToken: locals.csrfToken, 4 | }; 5 | } 6 | 7 | export const actions = { 8 | default: async () => { 9 | return { success: true }; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if form?.success} 8 |

success

9 | {:else} 10 |

CSRF token value: {data.csrfToken}

11 |

HTML Form Submission Example:

12 |
13 | Form without CSRF (should fail): 14 | 15 | 16 |
17 |
18 |
19 | Form with incorrect CSRF (should fail): 20 | 21 | 22 | 23 |
24 |
25 |
26 | Form with CSRF (should succeed): 27 | 28 | 29 | 30 |
31 | {/if} 32 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/sveltekit-vercel/static/favicon.ico -------------------------------------------------------------------------------- /examples/sveltekit-vercel/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amorey/edge-csrf/4f265c6e05fad2d5108cdd7fc1dc79468a5f0352/examples/sveltekit-vercel/static/favicon.png -------------------------------------------------------------------------------- /examples/sveltekit-vercel/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-vercel'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter({ 12 | runtime: 'edge', 13 | }) 14 | } 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /examples/sveltekit-vercel/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "pnpm -r build", 5 | "clean": "pnpm -r exec rm -rf node_modules", 6 | "lint": "pnpm -r lint", 7 | "test": "pnpm -r test run", 8 | "test-all": "pnpm -r test-all" 9 | }, 10 | "devDependencies": { 11 | "@edge-runtime/vm": "^3.2.0", 12 | "@types/jest": "^29.5.14", 13 | "@types/node": "^20.17.1", 14 | "@typescript-eslint/eslint-plugin": "^7.18.0", 15 | "@typescript-eslint/parser": "^7.18.0", 16 | "eslint": "^8.57.1", 17 | "eslint-config-airbnb-base": "^15.0.0", 18 | "eslint-config-airbnb-typescript": "^18.0.0", 19 | "tsup": "^8.3.4", 20 | "typescript": "^5.6.3", 21 | "vite": "^5.4.10", 22 | "vitest": "^1.6.0", 23 | "vitest-environment-miniflare": "^2.14.4" 24 | }, 25 | "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Core API 2 | 3 | This is the documentation for Edge-CSRF's low-level API. 4 | 5 | ## Install 6 | 7 | ```console 8 | npm install @edge-csrf/core 9 | # or 10 | pnpm add @edge-csrf/core 11 | # or 12 | yarn add @edge-csrf/core 13 | ``` 14 | 15 | ## Documentation 16 | 17 | The following methods are named exports in the the `@edge-csrf/core` module: 18 | 19 | ``` 20 | createSecret(length) - Create new secret (cryptographically secure) 21 | 22 | * @param {int} length - Byte length of secret 23 | * @returns {Uint8Array} - The secret 24 | 25 | createToken(secret, saltByteLength) - Create new CSRF token (cryptographically insecure 26 | salt hashed with secret) 27 | 28 | * @param {Uint8Array} secret - The secret 29 | * @param {int} saltByteLength - Salt length in number of bytes 30 | * @returns {Promise} - A promise returning the token in Uint8Array format 31 | 32 | getTokenString(request) - Get the CSRF token from the request 33 | 34 | * @param {Request} request - The request object 35 | * @returns {Promise} - A promise returning the token in string format 36 | 37 | verifyToken(token, secret) - Verify the CSRF token and secret obtained from the request 38 | 39 | * @param {Uint8Array} token - The CSRF token 40 | * @param {Uint8Array} secret - The CSRF secret 41 | * @returns {Promise} - A promise returning result of verification 42 | 43 | utoa(input) - Encode Uint8Array as base64 string 44 | 45 | * @param {Uint8Array} input - The data to be converted from Uint8Array to base64 46 | * @returns {string} The base64 encoded string 47 | 48 | atou(input) - Decode base64 string into Uint8Array 49 | 50 | * @param {string} input - The data to be converted from base64 to Uint8Array 51 | * @returns {Uint8Array} - The Uint8Array representing the input string 52 | ``` 53 | 54 | __Note__: If you're using these methods you're probably working on a custom framework integration. If so, please consider contributing it back to this project! 55 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edge-csrf/core", 3 | "version": "0.0.0", 4 | "description": "Edge-CSRF core API", 5 | "author": "Andres Morey", 6 | "license": "MIT", 7 | "repository": "kubetail-org/edge-csrf", 8 | "type": "module", 9 | "sideEffects": false, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "/dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc && vite build", 18 | "lint": "eslint \"./src/**/*.ts{,x}\"" 19 | }, 20 | "keywords": [ 21 | "csrf", 22 | "tokens", 23 | "edge" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | // util exports 2 | export { 3 | createSecret, 4 | createToken, 5 | getTokenString, 6 | verifyToken, 7 | utoa, 8 | atou, 9 | } from '@shared/util'; 10 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import dts from '../../shared/src/vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | '@shared': resolve(__dirname, '../../shared/src'), 11 | }, 12 | }, 13 | plugins: [dts()], 14 | build: { 15 | lib: { 16 | entry: [ 17 | resolve(__dirname, 'src/index.ts'), 18 | ], 19 | name: '@edge-csrf/core', 20 | formats: ['es', 'cjs'], 21 | }, 22 | rollupOptions: { 23 | external: [], 24 | }, 25 | }, 26 | test: { 27 | environment: 'edge-runtime', 28 | globals: true, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edge-csrf/express", 3 | "version": "0.0.0", 4 | "description": "Edge-CSRF Express integration library", 5 | "author": "Andres Morey", 6 | "license": "MIT", 7 | "repository": "kubetail-org/edge-csrf", 8 | "type": "module", 9 | "sideEffects": false, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "/dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc && vite build", 18 | "lint": "eslint \"./src/**/*.ts{,x}\"", 19 | "test": "vitest", 20 | "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" 21 | }, 22 | "keywords": [ 23 | "csrf", 24 | "tokens", 25 | "edge", 26 | "express" 27 | ], 28 | "dependencies": { 29 | "cookie": "^0.7.0" 30 | }, 31 | "devDependencies": { 32 | "@types/cookie": "^0.6.0", 33 | "@types/express": "^4.17.21", 34 | "@types/supertest": "^6.0.2", 35 | "express": "^4.19.2", 36 | "supertest": "^7.0.0" 37 | }, 38 | "peerDependencies": { 39 | "express": "^4.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/express/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import dts from '../../shared/src/vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | '@shared': resolve(__dirname, '../../shared/src'), 11 | }, 12 | }, 13 | plugins: [dts()], 14 | build: { 15 | lib: { 16 | entry: [ 17 | resolve(__dirname, 'src/index.ts'), 18 | ], 19 | name: '@edge-csrf/express', 20 | formats: ['es', 'cjs'], 21 | }, 22 | rollupOptions: { 23 | external: ['express'], 24 | }, 25 | }, 26 | test: { 27 | environment: 'edge-runtime', 28 | globals: true, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edge-csrf/nextjs", 3 | "version": "0.0.0", 4 | "description": "Edge-CSRF Next.js integration library", 5 | "author": "Andres Morey", 6 | "license": "MIT", 7 | "repository": "kubetail-org/edge-csrf", 8 | "type": "module", 9 | "sideEffects": false, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "/dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc && vite build", 18 | "lint": "eslint \"./src/**/*.ts{,x}\"", 19 | "test": "vitest", 20 | "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" 21 | }, 22 | "devDependencies": { 23 | "next": "^15.0.0" 24 | }, 25 | "peerDependencies": { 26 | "next": "^13.0.0 || ^14.0.0 || ^15.0.0" 27 | }, 28 | "keywords": [ 29 | "csrf", 30 | "tokens", 31 | "edge", 32 | "nextjs", 33 | "next" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/nextjs/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import dts from '../../shared/src/vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | '@shared': resolve(__dirname, '../../shared/src'), 11 | }, 12 | }, 13 | plugins: [dts()], 14 | build: { 15 | lib: { 16 | entry: [ 17 | resolve(__dirname, 'src/index.ts'), 18 | ], 19 | name: '@edge-csrf/nextjs', 20 | formats: ['es', 'cjs'], 21 | }, 22 | rollupOptions: { 23 | external: ['next/server'], 24 | }, 25 | }, 26 | test: { 27 | environment: 'edge-runtime', 28 | globals: true, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/node-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edge-csrf/node-http", 3 | "version": "0.0.0", 4 | "description": "Edge-CSRF integration library for node's http module", 5 | "author": "Andres Morey", 6 | "license": "MIT", 7 | "repository": "kubetail-org/edge-csrf", 8 | "type": "module", 9 | "sideEffects": false, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "/dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc && vite build", 18 | "lint": "eslint \"./src/**/*.ts{,x}\"", 19 | "test": "vitest", 20 | "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" 21 | }, 22 | "keywords": [ 23 | "csrf", 24 | "tokens", 25 | "edge", 26 | "node", 27 | "createServer" 28 | ], 29 | "dependencies": { 30 | "cookie": "^0.7.0" 31 | }, 32 | "devDependencies": { 33 | "@types/cookie": "^0.6.0", 34 | "@types/supertest": "^6.0.2", 35 | "supertest": "^7.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/node-http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/node-http/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import dts from '../../shared/src/vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | '@shared': resolve(__dirname, '../../shared/src'), 11 | }, 12 | }, 13 | plugins: [dts()], 14 | build: { 15 | lib: { 16 | entry: [ 17 | resolve(__dirname, 'src/index.ts'), 18 | ], 19 | name: '@edge-csrf/node-http', 20 | formats: ['es', 'cjs'], 21 | } 22 | }, 23 | test: { 24 | environment: 'edge-runtime', 25 | globals: true, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /packages/sveltekit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edge-csrf/sveltekit", 3 | "version": "0.0.0", 4 | "description": "Edge-CSRF SvelteKit integration library", 5 | "author": "Andres Morey", 6 | "license": "MIT", 7 | "repository": "kubetail-org/edge-csrf", 8 | "type": "module", 9 | "sideEffects": false, 10 | "main": "dist/index.cjs", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "files": [ 14 | "/dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc && vite build", 18 | "lint": "eslint \"./src/**/*.ts{,x}\"", 19 | "test": "vitest", 20 | "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" 21 | }, 22 | "devDependencies": { 23 | "@sveltejs/kit": "^2.5.6" 24 | }, 25 | "keywords": [ 26 | "csrf", 27 | "tokens", 28 | "edge", 29 | "sveltekit", 30 | "svelte" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/sveltekit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/sveltekit/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import dts from '../../shared/src/vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | '@shared': resolve(__dirname, '../../shared/src'), 11 | }, 12 | }, 13 | plugins: [dts()], 14 | build: { 15 | lib: { 16 | entry: [ 17 | resolve(__dirname, 'src/index.ts'), 18 | ], 19 | name: '@edge-csrf/sveltekit', 20 | formats: ['es', 'cjs'], 21 | }, 22 | rollupOptions: { 23 | external: [], 24 | }, 25 | }, 26 | test: { 27 | environment: 'edge-runtime', 28 | globals: true, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**" 3 | - "shared" 4 | - "!examples/**" 5 | -------------------------------------------------------------------------------- /set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if a version number was provided as an argument 4 | if [ "$#" -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | # Assign the new version number from command line arguments 10 | NEW_VERSION=$1 11 | 12 | # Function to update the version in a package.json file 13 | update_version() { 14 | local file=$1 15 | echo "Updating version in $file to $NEW_VERSION" 16 | # Portable handling of in-place editing with sed 17 | sed -i.bak -E "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" "$file" && rm "$file.bak" 18 | } 19 | 20 | export -f update_version 21 | export NEW_VERSION 22 | 23 | # Find all package.json files in the packages directory, excluding node_modules directories 24 | find ./packages -name "package.json" -not -path "*/node_modules/*" -exec bash -c 'update_version "$0"' {} \; 25 | 26 | echo "Version update completed." 27 | -------------------------------------------------------------------------------- /shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@edge-csrf/shared", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "lint": "eslint \"./src/**/*.ts{,x}\"", 7 | "test": "vitest", 8 | "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /shared/src/vite-plugin-dts.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'tsup'; 2 | import type { Plugin } from 'vite'; 3 | 4 | export default (): Plugin => ({ 5 | name: 'dts', 6 | apply: 'build', 7 | closeBundle: async () => { 8 | await build({ 9 | entry: ['src/index.ts'], 10 | outDir: 'dist', 11 | format: 'esm', 12 | dts: { 13 | only: true, 14 | }, 15 | }); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /shared/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | resolve: { 7 | alias: { 8 | '@shared': resolve(__dirname, '../../shared/src'), 9 | }, 10 | }, 11 | test: { 12 | environment: 'edge-runtime', 13 | globals: true, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2020", 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | 14 | /* bundler mode */ 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | 20 | /* linting */ 21 | "strict": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noImplicitAny": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | 28 | /* import paths */ 29 | "baseUrl": "./", 30 | "paths": { 31 | "@shared/*": [ 32 | "shared/src/*" 33 | ] 34 | }, 35 | 36 | /* misc */ 37 | "types": [ 38 | "vitest/globals" 39 | ] 40 | }, 41 | "exclude": [ 42 | "node_modules", 43 | "dist" 44 | ] 45 | } --------------------------------------------------------------------------------