Repository: j0lv3r4/next-csrf
Branch: main
Commit: f3a9288bba25
Files: 36
Total size: 43.1 KB
Directory structure:
gitextract_4a39ddmd/
├── .eslintrc.js
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── .npmignore
├── LICENSE.md
├── README.md
├── example/
│ ├── .gitignore
│ ├── components/
│ │ └── layout.js
│ ├── lib/
│ │ └── csrf.js
│ ├── package.json
│ ├── pages/
│ │ ├── _app.js
│ │ ├── api/
│ │ │ ├── csrf/
│ │ │ │ └── setup.js
│ │ │ ├── hello.js
│ │ │ └── protected.js
│ │ ├── index.js
│ │ └── login.js
│ └── styles/
│ ├── Home.module.css
│ └── globals.css
├── jest.config.js
├── package.json
├── playwright.config.ts
├── rollup.config.js
├── src/
│ ├── index.e2e.ts
│ ├── index.ts
│ ├── middleware/
│ │ ├── csrf.test.ts
│ │ ├── csrf.ts
│ │ ├── index.ts
│ │ └── setup.ts
│ ├── package.json
│ ├── types.ts
│ └── utils/
│ ├── create-token.ts
│ ├── get-cookie.ts
│ ├── get-secret.ts
│ ├── httpError.ts
│ └── index.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
module.exports = {
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [jOlv3r4] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .gitignore
================================================
#### joe made this: http://goel.io/joe
#### linux ####
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
#### macos ####
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
#### node ####
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
@types
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
#### vim ####
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
#### visualstudiocode ####
.vscode/*
dist
.idea
.now
example/.vercel
#### yalc ####
example/.yalc
**/.yalc/**/*.md
================================================
FILE: .npmignore
================================================
rollup.config.js
src
package-lock.json
yarn.lock
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2022 Juan Olvera
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
================================================
FILE: README.md
================================================
# next-csrf

CSRF mitigation for Next.js.
## Features
Mitigation patterns that `next-csrf` implements:
* [Synchronizer Token Pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern) using [`csrf`](https://github.com/pillarjs/csrf) (Also [read Understanding CSRF](https://github.com/pillarjs/understanding-csrf#csrf-tokens))
## Installation
With yarn:
```bash
yarn add next-csrf
```
With npm:
```bash
npm i next-csrf --save
```
## Usage
Create an initialization file to add options:
```js
// file: lib/csrf.js
import { nextCsrf } from "next-csrf";
const { csrf, setup } = nextCsrf({
// eslint-disable-next-line no-undef
secret: process.env.CSRF_SECRET,
});
export { csrf, setup };
```
Protect an API endpoint:
```js
// file: pages/api/protected.js
import { csrf } from '../lib/csrf';
const handler = (req, res) => {
return res.status(200).json({ message: "This API route is protected."})
}
export default csrf(handler);
```
Test the protected API route by sending a POST request from your terminal. Since this request doesn't have the proper token setup, it wil fail.
```shell
curl -X POST http://localhost:3000/api/protected
>> {"message": "Invalid CSRF token"}
```
Use an [SSG page](https://nextjs.org/docs/basic-features/pages#server-side-rendering) to set up the token. Usually, you use CSRF mitigation to harden your requests from authenticated users, if this is the case then you should use the login page.
```js
// file: pages/login.js
import { setup } from '../lib/csrf';
function Login() {
const loginRequest = async (event) => {
event.preventDefault();
// The secret and token are sent with the request by default, so no extra
// configuration is needed in the request.
const response = await fetch('/api/protected', {
method: 'post'
});
if (response.ok) {
console.log('protected response ok');
}
}
return (
<form onSubmit={loginRequest}>
<label>
Username
<input type="text" required />
</label>
<label>
Password
<input type="password" required />
</label>
<button>Submit</button>
</form>
)
}
// Here's the important part. `setup` saves the necesary secret and token.
export const getServerSideProps = setup(async ({req, res}) => {
return { props: {}}
});
export default Login;
```
## API
### `nextCsrf(options);`
Returns two functions:
* `setup` Setups two cookies, one for the secret and other one for the token. Only works on SSG pages.
* `csrf` Protects API routes from requests without the token. Validates and verify signatures on the cookies.
#### `options`
* `tokenKey` (`string`) The name of the cookie to store the CSRF token. Default is `"XSRF-TOKEN"`.
* `csrfErrorMessage` (`string`) Error message to return for unauthorized requests. Default is `"Invalid CSRF token"`.
* `ignoredMethods`: (`string[]`) Methods to ignore, i.e. let pass all requests with these methods. Default is `["GET", "HEAD", "OPTIONS"]`.
* `cookieOptions`: Same options as https://www.npmjs.com/package/cookie
================================================
FILE: example/.gitignore
================================================
.vercel
.yalc
================================================
FILE: example/components/layout.js
================================================
import Head from "next/head";
import Link from "next/link";
import styles from "../styles/Home.module.css";
function Layout({ children }) {
return (
<div className={styles.container}>
<Head>
<title>Next CSRF</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<header>
<h1 className={styles.title}>Next CSRF</h1>
<nav className={styles.nav}>
<Link href="/">Home</Link>
<Link href="login">Login</Link>
</nav>
</header>
<div>{children}</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{" "}
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
</a>
</footer>
</div>
);
}
export default Layout;
================================================
FILE: example/lib/csrf.js
================================================
import { nextCsrf } from "next-csrf";
const { csrf, setup } = nextCsrf({
// eslint-disable-next-line no-undef
secret: process.env.CSRF_SECRET,
});
export { csrf, setup };
================================================
FILE: example/package.json
================================================
{
"name": "next-csrf-example",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "9.5.3",
"next-csrf": "file:.yalc/next-csrf",
"react": "16.13.1",
"react-dom": "16.13.1"
},
"devDependencies": {
"prettier": "^2.1.1"
}
}
================================================
FILE: example/pages/_app.js
================================================
import Layout from "../components/layout";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
================================================
FILE: example/pages/api/csrf/setup.js
================================================
import { setup } from "../../../lib/csrf";
const handler = (req, res) => {
res.statusCode = 200;
res.json({ message: "CSRF token added to cookies" });
};
export default setup(handler);
================================================
FILE: example/pages/api/hello.js
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default (req, res) => {
res.statusCode = 200;
res.json({ name: "John Doe" });
};
================================================
FILE: example/pages/api/protected.js
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { csrf } from "../../lib/csrf";
const handler = (req, res) => {
res.statusCode = 200;
res.json({ message: "Request successful" });
};
export default csrf(handler);
================================================
FILE: example/pages/index.js
================================================
import styles from "../styles/Home.module.css";
import Link from "next/link";
export default function Home() {
return (
<>
<p className={styles.description}>
Get started by editing{" "}
<code className={styles.code}>pages/index.js</code>
</p>
<div>
<div className={styles.card}>
<h3>Send a request without the CSRF token</h3>
<p>
Because any request we send from the browser will have a cookie with
the token attached, try to send a request from a terminal and see
what happens with a missing or an invalid CSRF token.
</p>
<pre>
<code className={styles.code}>
$ curl -X POST http://localhost:3000/api/protected
</code>
</pre>
<pre>
<code className={styles.code}>
{`>> {"message": "Invalid CSRF token"}`}
</code>
</pre>
</div>
<div className={styles.card}>
<h3>Send a request with the proper CSRF token setup</h3>
<ol>
<li>
Go to the <Link href="login">Login</Link> page
</li>
<li>Open the Console</li>
<li>
Fill the form with "demo" for username and "demo" for password
</li>
<li>Submit the form, inspect the Console and the Network</li>
<li>
Try to modify the cookie values, try again, and see the request
being blocked.
</li>
</ol>
</div>
</div>
<style jsx>{`
ol li {
margin-bottom: 0.8rem;
}
`}</style>
</>
);
}
================================================
FILE: example/pages/login.js
================================================
import styles from "../styles/Home.module.css";
import { setup } from "../lib/csrf";
function Login() {
const requestWithToken = async (event) => {
event.preventDefault();
const response = await fetch("/api/protected", {
method: "post",
});
if (response.ok) {
console.log("protected response ok");
console.log(response);
}
};
return (
<>
<p>Use "demo" for both username and password</p>
<form className={styles.form} onSubmit={requestWithToken}>
<label>
Username
<input type="text" required />
</label>
<label htmlFor="password">
Password
<input type="password" required />
</label>
<button>Submit</button>
</form>
</>
);
}
export const getServerSideProps = setup(async ({ req, res }) => {
return { props: {} };
});
export default Login;
================================================
FILE: example/styles/Home.module.css
================================================
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.form .info {
color: #333;
}
.form {
border: 1px solid #ccc;
padding: 1rem;
}
.form label {
padding: .5rem 0;
display: flex;
flex-direction: column;
}
.form input {
margin-top: .5rem;
}
.form button {
margin-top: .5rem;
}
.nav {
display: flex;
justify-content: center;
padding-top: 1rem;
padding-bottom: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.nav a {
text-decoration: underline;
color: blue;
margin-right: .5rem;
}
.main {
max-width: 45rem;
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.button {
padding: .5rem 1rem;
border: 1px solid black;
background: black;
color: white;
font-size: 1.2rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin-top: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
================================================
FILE: example/styles/globals.css
================================================
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
================================================
FILE: jest.config.js
================================================
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// Respect "browser" field in package.json when resolving modules
// browser: false,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
moduleFileExtensions: [
"js",
// "json",
// "jsx",
"ts",
"tsx",
// "node"
],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
transform: {
".(ts|tsx)": "ts-jest",
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
================================================
FILE: package.json
================================================
{
"name": "next-csrf",
"version": "0.2.1",
"description": "CSRF mitigation library for Next.js",
"main": "dist/next-csrf.js",
"module": "dist/next-csrf.esm.js",
"types": "@types/",
"source": "src/index.ts",
"repository": "https://github.com/j0lv3r4/next-csrf",
"author": "Juan Olvera",
"license": "MIT",
"scripts": {
"build": "npm run types && rollup -c",
"dev": "rollup -w",
"test": "jest",
"test:e2e": "playwright test",
"types": "tsc --emitDeclarationOnly --declaration --outDir @types",
"release": "cross-var npm run build && cross-var git commit -am $npm_package_version && cross-var git tag $npm_package_version && git push && git push --tags && npm publish"
},
"devDependencies": {
"@playwright/test": "^1.20.0",
"@rollup/plugin-commonjs": "^13.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-typescript": "^5.0.2",
"@types/cookie": "^0.4.0",
"@types/cookie-signature": "^1.0.3",
"@types/csrf": "^1.3.2",
"@types/jest": "^26.0.15",
"@types/supertest": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"axios": "^0.26.1",
"eslint": "^7.12.1",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-plugin-import": "^2.22.1",
"jest": "^26.6.1",
"next": "^9.5.5",
"np": "^6.5.0",
"prettier": "^2.1.2",
"rollup": "^2.32.1",
"rollup-plugin-analyzer": "^3.3.0",
"supertest": "^4.0.2",
"ts-jest": "^26.4.3",
"typescript": "^3.9.7"
},
"dependencies": {
"@types/http-errors": "^1.8.0",
"cookie": "^0.4.1",
"cookie-signature": "^1.1.0",
"csrf": "^3.1.0",
"querystring": "^0.2.0"
}
}
================================================
FILE: playwright.config.ts
================================================
import { PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
testMatch: "**/*.e2e.ts",
webServer: {
command: "npm --prefix example run dev",
port: 3000,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
};
export default config;
================================================
FILE: rollup.config.js
================================================
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import analyze from "rollup-plugin-analyzer";
import pkg from "./package.json";
export default [
{
input: "src/index.ts",
output: [
{ file: pkg.main, format: "cjs" },
{ file: pkg.module, format: "es" },
],
plugins: [typescript(), commonjs(), resolve(), analyze()],
external: ["crypto", "util"],
},
];
================================================
FILE: src/index.e2e.ts
================================================
import { test, expect, request } from "@playwright/test";
import axios from "axios";
// Go to login page
// Check for secret and token
// Submit the form
// Expect 200
// Go to login page
// Check for secret and token
// Modify token
// Expect 403
// Go to login page
// Check for secret and token
// Modify token
// Expect 403
// Send a GET request to /login
// Grab the cookies
// Send a POST request to /api/protected with the same cookies
// Expect 200
// Send a GET request to /login
// Grab the cookies
// Modify the cookies
// Send a POST request to /api/protected with the same cookies
// Expect 403
test("config is setup correctly in the example", async ({ page, baseURL }) => {
if (baseURL != null) {
await page.goto(baseURL);
const [response] = await Promise.all([
page.waitForResponse((res) => res.status() === 200),
]);
expect(response.status()).toBe(200);
}
});
================================================
FILE: src/index.ts
================================================
import { NextApiHandler } from "next";
import { csrf, setup } from "./middleware";
import { NextCsrfOptions } from "./types";
import { CookieSerializeOptions } from "cookie";
const cookieDefaultOptions: CookieSerializeOptions = {
httpOnly: true,
path: "/",
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
};
const defaultOptions = {
tokenKey: "XSRF-TOKEN",
csrfErrorMessage: "Invalid CSRF token",
ignoredMethods: ["GET", "HEAD", "OPTIONS"],
cookieOptions: cookieDefaultOptions,
};
type Middleware = (handler: NextApiHandler) => void;
type NextCSRF = {
setup: Middleware;
csrf: Middleware;
};
function nextCsrf(userOptions: NextCsrfOptions): NextCSRF {
const options = {
...defaultOptions,
...userOptions,
};
// generate middleware
return {
setup: (handler: NextApiHandler) =>
setup(handler, {
tokenKey: options.tokenKey,
cookieOptions: options.cookieOptions,
secret: options.secret,
}),
csrf: (handler: NextApiHandler) => csrf(handler, options),
};
}
export { nextCsrf };
================================================
FILE: src/middleware/csrf.test.ts
================================================
import request from "supertest";
import http, { IncomingMessage, ServerResponse } from "http";
import { apiResolver } from "next/dist/next-server/server/api-utils";
import { NextApiRequest, NextApiResponse } from "next";
import { nextCsrf } from "../index";
describe("setup middleware", () => {
const secret = "yoMqR8xtQUhbmLwM*kRK";
const tokenKey = "XSRF-TOKEN";
const userOptions = {
secret,
tokenKey,
cookieOptions: { httpOnly: true, path: "/" },
};
const apiPreviewPropsMock = {
previewModeId: "id",
previewModeEncryptionKey: "key",
previewModeSigningKey: "key",
};
const { setup } = nextCsrf(userOptions);
const requestListener = (req: IncomingMessage, res: ServerResponse) => {
apiResolver(
req,
res,
undefined,
setup((req: NextApiRequest, res: NextApiResponse) => {
return res.status(200).json({ message: "Hello, world." });
}),
apiPreviewPropsMock,
true
);
};
it.only("Should setup cookies with csrfSecret and token", async () => {
const server = http.createServer(requestListener);
const agent = await request.agent(server).post("/");
expect(agent.header["set-cookie"][0]).toEqual(
expect.stringMatching(/csrfSecret=(.+); Path=\/; HttpOnly/g)
);
expect(agent.header["set-cookie"][1]).toEqual(
expect.stringMatching(/XSRF-TOKEN=(.+); Path=\/; HttpOnly/g)
);
expect(agent.text).toBe(JSON.stringify({ message: "Hello, world." }));
});
});
describe("csrf middleware", () => {
const secret = "yoMqR8xtQUhbmLwM*kRK";
const tokenKey = "XSRF-TOKEN";
const userOptions = {
secret,
tokenKey,
cookieOptions: { httpOnly: true, path: "/" },
};
// mock for `apiResolver`'s 5th parameter to please TS
const apiPreviewPropsMock = {
previewModeId: "id",
previewModeEncryptionKey: "key",
previewModeSigningKey: "key",
};
const { csrf } = nextCsrf(userOptions);
const requestListener = (req: IncomingMessage, res: ServerResponse) => {
apiResolver(
req,
res,
undefined,
csrf((req: NextApiRequest, res: NextApiResponse) => {
return res.status(200).json({ message: "Hello, world." });
}),
apiPreviewPropsMock,
true
);
};
it("should setup a CSRF token on the first request, i.e. when there's no token already in a cookie", async () => {
// If we receive a request without secret in a cookie we assume it's the first request to an API route
const server = http.createServer(requestListener);
const agent = await request.agent(server).post("/");
console.log(agent.status);
expect(agent.header["set-cookie"][0]).toEqual(
expect.stringMatching(/XSRF-TOKEN=(.+); Path=\/; HttpOnly/g)
);
expect(agent.text).toBe(JSON.stringify({ message: "Hello, world." }));
});
it("should validate a token with a secret and send 200 if everything is okay after the first request", async () => {
const server = http.createServer(requestListener);
const firstRequest = await request.agent(server).get("/");
// Grab the token and secret from the response
const [reqCsrfToken] = firstRequest.header["set-cookie"];
// Send back the token in a header and a cookie
const secondRequest = await request
.agent(server)
.get("/")
.set("Cookie", reqCsrfToken)
.set(tokenKey, reqCsrfToken);
expect(secondRequest.status).toBe(200);
});
it("should validate a token with a secret and send 200 even if the cookie exists but is different from the XSRF token", async () => {
const server = http.createServer(requestListener);
const firstRequest = await request.agent(server).get("/");
// Grab the token and secret from the response
const [reqCsrfToken] = firstRequest.header["set-cookie"];
// Send back the token in a header and a cookie
const secondRequest = await request
.agent(server)
.get("/")
.set("Cookie", "anotherCookieThanXSRF")
.set(tokenKey, reqCsrfToken);
expect(secondRequest.status).toBe(200);
});
it("should return 403 if we don't send a valid token in a custom header and a cookie", async () => {
const server = http.createServer(requestListener);
const firstRequest = await request.agent(server).post("/");
// Grab the token and secret from the response
const [reqCsrfToken] = firstRequest.header["set-cookie"];
const [token, signature] = reqCsrfToken.split(".");
const tamperedToken = `${token}.invalidSignature`;
// Request without token in a custom header
const secondRequest = await request
.agent(server)
.post("/")
.set("Cookie", reqCsrfToken);
// Request with an invalid token in a custom header
const thirdRequest = await request
.agent(server)
.post("/")
.set("Cookie", reqCsrfToken)
.set(tokenKey, tamperedToken);
// Request with an invalid token in the cookie
const fourthRequest = await request
.agent(server)
.post("/")
.set("Cookie", tamperedToken)
.set(tokenKey, reqCsrfToken);
expect(secondRequest.status).toBe(403);
expect(thirdRequest.status).toBe(403);
expect(fourthRequest.status).toBe(403);
});
});
================================================
FILE: src/middleware/csrf.ts
================================================
import { HttpError } from "../utils";
import { serialize, parse } from "cookie";
import { sign, unsign } from "cookie-signature";
import { createToken } from "../utils/create-token";
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { MiddlewareArgs } from "../types";
const csrf = (
handler: NextApiHandler,
{
ignoredMethods,
csrfErrorMessage,
tokenKey,
cookieOptions,
secret,
}: MiddlewareArgs
) => async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
try {
if (typeof req.method !== "string") {
throw new HttpError(403, csrfErrorMessage);
}
// Do nothing on if method is in `ignoreMethods`
if (ignoredMethods.includes(req.method)) {
return handler(req, res);
}
// Fail if no cookie is present
if (req.headers?.cookie === undefined) {
throw new HttpError(403, csrfErrorMessage);
}
const cookie = parse(req.headers?.cookie);
// Extract secret and token from their cookies
let token = cookie[tokenKey];
const csrfSecret = cookie["csrfSecret"];
// Check token is in the cookie
if (!token) {
throw new HttpError(403, csrfErrorMessage);
}
// If user provided a secret, then the cookie is signed.
// Unsign and verify aka Synchronizer token pattern.
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
if (secret != null) {
// unsign cookie
const unsignedToken = unsign(token, secret);
// validate signature
if (!unsignedToken) {
throw new HttpError(403, csrfErrorMessage);
}
token = unsignedToken;
}
// 5. verify CSRF token
if (!createToken.verify(csrfSecret, token)) {
throw new HttpError(403, csrfErrorMessage);
}
// If token is verified, generate a new one and save it in the cookie
let newToken;
if (secret != null) {
// Sign if `secret` is present
newToken = sign(createToken.create(csrfSecret), secret);
} else {
newToken = createToken.create(csrfSecret);
}
res.setHeader("Set-Cookie", serialize(tokenKey, newToken, cookieOptions));
return handler(req, res);
} catch (error) {
return res.status(error.status ?? 500).json({ message: error.message });
}
};
export { csrf };
================================================
FILE: src/middleware/index.ts
================================================
export * from "./csrf";
export * from "./setup";
================================================
FILE: src/middleware/setup.ts
================================================
import {
GetServerSidePropsContext,
NextApiHandler,
NextApiRequest,
NextApiResponse,
} from "next";
import { SetupMiddlewareArgs } from "../types";
import { createToken } from "../utils/create-token";
import { sign } from "cookie-signature";
import { serialize } from "cookie";
import { getSecret } from "../utils/get-secret";
type SetupArgs =
| NextApiRequest[]
| NextApiResponse[]
| GetServerSidePropsContext[];
const setup = (
handler: NextApiHandler,
{ secret, tokenKey, cookieOptions }: SetupMiddlewareArgs
) => async (...args: SetupArgs): Promise<void> => {
const isApi = args.length > 1;
const req = isApi
? (args[0] as NextApiRequest) // (*req*, res)
: (args[0] as GetServerSidePropsContext).req; // (context).req
const res = isApi
? (args[1] as NextApiResponse) // (req, *res*)
: (args[0] as GetServerSidePropsContext).res; // (context).res
const csrfSecret = getSecret(req, "csrfSecret") || createToken.secretSync();
const unsignedToken = createToken.create(csrfSecret);
// TODO:
// Make a note that if the user changes the secret in the backend all the sessions
// will invalidate
let token;
if (secret != null) {
token = sign(unsignedToken, secret);
} else {
token = unsignedToken;
}
res.setHeader("Set-Cookie", [
serialize("csrfSecret", csrfSecret, cookieOptions),
serialize(tokenKey, token, cookieOptions),
]);
return handler(req as NextApiRequest, res as NextApiResponse);
};
export { setup };
================================================
FILE: src/package.json
================================================
{
"name": "src",
"version": "1.0.0",
"dependencies": {
}
}
================================================
FILE: src/types.ts
================================================
import { CookieSerializeOptions } from "cookie";
interface NextCsrfOptions {
ignoredMethods?: string[];
csrfErrorMessage?: string;
tokenKey?: string;
cookieOptions?: CookieSerializeOptions;
secret?: string;
}
// Make the optional parameters in `nextCsrf` required in the `csrf` middleware
interface MiddlewareArgs extends NextCsrfOptions {
csrfErrorMessage: string;
ignoredMethods: string[];
tokenKey: string;
cookieOptions: CookieSerializeOptions;
}
interface SetupMiddlewareArgs {
tokenKey: string;
cookieOptions: CookieSerializeOptions;
secret?: string;
}
export { NextCsrfOptions, MiddlewareArgs, SetupMiddlewareArgs };
================================================
FILE: src/utils/create-token.ts
================================================
import Tokens from "csrf";
const createToken = new Tokens();
export { createToken };
================================================
FILE: src/utils/get-cookie.ts
================================================
import { parse } from "cookie";
import { NextApiRequest } from "next";
import { IncomingMessage } from "http";
function getCookie(req: IncomingMessage, name: string): string {
if (req.headers.cookie != null) {
const parsedCookie = parse(req.headers.cookie);
return parsedCookie[name];
}
return "";
}
export { getCookie };
================================================
FILE: src/utils/get-secret.ts
================================================
import { getCookie } from "./get-cookie";
import { IncomingMessage } from "http";
const getSecret = (req: IncomingMessage, tokenKey: string): string => {
return getCookie(req, tokenKey.toLowerCase());
};
export { getSecret };
================================================
FILE: src/utils/httpError.ts
================================================
class HttpError extends Error {
private status: number;
constructor(status = 403, message: string, ...params: undefined[]) {
super(...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError);
}
this.name = "HttpError";
this.status = status;
this.message = message;
}
}
export { HttpError };
================================================
FILE: src/utils/index.ts
================================================
export * from "./httpError";
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
]
}
gitextract_4a39ddmd/ ├── .eslintrc.js ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── example/ │ ├── .gitignore │ ├── components/ │ │ └── layout.js │ ├── lib/ │ │ └── csrf.js │ ├── package.json │ ├── pages/ │ │ ├── _app.js │ │ ├── api/ │ │ │ ├── csrf/ │ │ │ │ └── setup.js │ │ │ ├── hello.js │ │ │ └── protected.js │ │ ├── index.js │ │ └── login.js │ └── styles/ │ ├── Home.module.css │ └── globals.css ├── jest.config.js ├── package.json ├── playwright.config.ts ├── rollup.config.js ├── src/ │ ├── index.e2e.ts │ ├── index.ts │ ├── middleware/ │ │ ├── csrf.test.ts │ │ ├── csrf.ts │ │ ├── index.ts │ │ └── setup.ts │ ├── package.json │ ├── types.ts │ └── utils/ │ ├── create-token.ts │ ├── get-cookie.ts │ ├── get-secret.ts │ ├── httpError.ts │ └── index.ts └── tsconfig.json
SYMBOL INDEX (14 symbols across 9 files)
FILE: example/components/layout.js
function Layout (line 5) | function Layout({ children }) {
FILE: example/pages/_app.js
function MyApp (line 4) | function MyApp({ Component, pageProps }) {
FILE: example/pages/index.js
function Home (line 4) | function Home() {
FILE: example/pages/login.js
function Login (line 4) | function Login() {
FILE: src/index.ts
type Middleware (line 20) | type Middleware = (handler: NextApiHandler) => void;
type NextCSRF (line 22) | type NextCSRF = {
function nextCsrf (line 27) | function nextCsrf(userOptions: NextCsrfOptions): NextCSRF {
FILE: src/middleware/setup.ts
type SetupArgs (line 13) | type SetupArgs =
FILE: src/types.ts
type NextCsrfOptions (line 3) | interface NextCsrfOptions {
type MiddlewareArgs (line 12) | interface MiddlewareArgs extends NextCsrfOptions {
type SetupMiddlewareArgs (line 19) | interface SetupMiddlewareArgs {
FILE: src/utils/get-cookie.ts
function getCookie (line 5) | function getCookie(req: IncomingMessage, name: string): string {
FILE: src/utils/httpError.ts
class HttpError (line 1) | class HttpError extends Error {
method constructor (line 4) | constructor(status = 403, message: string, ...params: undefined[]) {
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
{
"path": ".eslintrc.js",
"chars": 686,
"preview": "module.exports = {\n \"env\": {\n \"browser\": true,\n \"es2020\": true\n },\n \"extends\": [\n \"eslint:"
},
{
"path": ".github/FUNDING.yml",
"chars": 813,
"preview": "# These are supported funding model platforms\n\ngithub: [jOlv3r4] # Replace with up to 4 GitHub Sponsors-enabled username"
},
{
"path": ".gitignore",
"chars": 2225,
"preview": "#### joe made this: http://goel.io/joe\n\n#### linux ####\n*~\n\n# temporary files which can be created if a process still ha"
},
{
"path": ".npmignore",
"chars": 49,
"preview": "rollup.config.js\nsrc\npackage-lock.json\nyarn.lock\n"
},
{
"path": "LICENSE.md",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2022 Juan Olvera\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 3395,
"preview": "# next-csrf\n\n\n\nCSRF mitigation for Next.js.\n\n## "
},
{
"path": "example/.gitignore",
"chars": 13,
"preview": ".vercel\n.yalc"
},
{
"path": "example/components/layout.js",
"chars": 1031,
"preview": "import Head from \"next/head\";\nimport Link from \"next/link\";\nimport styles from \"../styles/Home.module.css\";\n\nfunction La"
},
{
"path": "example/lib/csrf.js",
"chars": 177,
"preview": "import { nextCsrf } from \"next-csrf\";\n\nconst { csrf, setup } = nextCsrf({\n // eslint-disable-next-line no-undef\n secre"
},
{
"path": "example/package.json",
"chars": 362,
"preview": "{\n \"name\": \"next-csrf-example\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"bui"
},
{
"path": "example/pages/_app.js",
"chars": 222,
"preview": "import Layout from \"../components/layout\";\nimport \"../styles/globals.css\";\n\nfunction MyApp({ Component, pageProps }) {\n "
},
{
"path": "example/pages/api/csrf/setup.js",
"chars": 191,
"preview": "import { setup } from \"../../../lib/csrf\";\n\nconst handler = (req, res) => {\n res.statusCode = 200;\n res.json({ message"
},
{
"path": "example/pages/api/hello.js",
"chars": 171,
"preview": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\n\nexport default (req, res) => {\n res.stat"
},
{
"path": "example/pages/api/protected.js",
"chars": 255,
"preview": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport { csrf } from \"../../lib/csrf\";\n\nco"
},
{
"path": "example/pages/index.js",
"chars": 1709,
"preview": "import styles from \"../styles/Home.module.css\";\nimport Link from \"next/link\";\n\nexport default function Home() {\n return"
},
{
"path": "example/pages/login.js",
"chars": 900,
"preview": "import styles from \"../styles/Home.module.css\";\nimport { setup } from \"../lib/csrf\";\n\nfunction Login() {\n const request"
},
{
"path": "example/styles/Home.module.css",
"chars": 2408,
"preview": ".container {\n min-height: 100vh;\n padding: 0 0.5rem;\n display: flex;\n flex-direction: column;\n justify-content: cen"
},
{
"path": "example/styles/globals.css",
"chars": 275,
"preview": "html,\nbody {\n padding: 0;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n "
},
{
"path": "jest.config.js",
"chars": 6632,
"preview": "// For a detailed explanation regarding each configuration property, visit:\n// https://jestjs.io/docs/en/configuration.h"
},
{
"path": "package.json",
"chars": 1746,
"preview": "{\n \"name\": \"next-csrf\",\n \"version\": \"0.2.1\",\n \"description\": \"CSRF mitigation library for Next.js\",\n \"main\": \"dist/n"
},
{
"path": "playwright.config.ts",
"chars": 300,
"preview": "import { PlaywrightTestConfig } from \"@playwright/test\";\n\nconst config: PlaywrightTestConfig = {\n testMatch: \"**/*.e2e."
},
{
"path": "rollup.config.js",
"chars": 490,
"preview": "import resolve from \"@rollup/plugin-node-resolve\";\nimport commonjs from \"@rollup/plugin-commonjs\";\nimport typescript fro"
},
{
"path": "src/index.e2e.ts",
"chars": 910,
"preview": "import { test, expect, request } from \"@playwright/test\";\nimport axios from \"axios\";\n\n// Go to login page\n// Check for s"
},
{
"path": "src/index.ts",
"chars": 1077,
"preview": "import { NextApiHandler } from \"next\";\nimport { csrf, setup } from \"./middleware\";\nimport { NextCsrfOptions } from \"./ty"
},
{
"path": "src/middleware/csrf.test.ts",
"chars": 5243,
"preview": "import request from \"supertest\";\nimport http, { IncomingMessage, ServerResponse } from \"http\";\nimport { apiResolver } fr"
},
{
"path": "src/middleware/csrf.ts",
"chars": 2362,
"preview": "import { HttpError } from \"../utils\";\nimport { serialize, parse } from \"cookie\";\nimport { sign, unsign } from \"cookie-si"
},
{
"path": "src/middleware/index.ts",
"chars": 49,
"preview": "export * from \"./csrf\";\nexport * from \"./setup\";\n"
},
{
"path": "src/middleware/setup.ts",
"chars": 1497,
"preview": "import {\n GetServerSidePropsContext,\n NextApiHandler,\n NextApiRequest,\n NextApiResponse,\n} from \"next\";\nimport { Set"
},
{
"path": "src/package.json",
"chars": 67,
"preview": "{\n \"name\": \"src\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n }\n}\n"
},
{
"path": "src/types.ts",
"chars": 652,
"preview": "import { CookieSerializeOptions } from \"cookie\";\n\ninterface NextCsrfOptions {\n ignoredMethods?: string[];\n csrfErrorMe"
},
{
"path": "src/utils/create-token.ts",
"chars": 87,
"preview": "import Tokens from \"csrf\";\n\nconst createToken = new Tokens();\n\nexport { createToken };\n"
},
{
"path": "src/utils/get-cookie.ts",
"chars": 339,
"preview": "import { parse } from \"cookie\";\nimport { NextApiRequest } from \"next\";\nimport { IncomingMessage } from \"http\";\n\nfunction"
},
{
"path": "src/utils/get-secret.ts",
"chars": 230,
"preview": "import { getCookie } from \"./get-cookie\";\nimport { IncomingMessage } from \"http\";\n\nconst getSecret = (req: IncomingMessa"
},
{
"path": "src/utils/httpError.ts",
"chars": 355,
"preview": "class HttpError extends Error {\n private status: number;\n\n constructor(status = 403, message: string, ...params: undef"
},
{
"path": "src/utils/index.ts",
"chars": 29,
"preview": "export * from \"./httpError\";\n"
},
{
"path": "tsconfig.json",
"chars": 6095,
"preview": "{\n \"compilerOptions\": {\n /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n /* Basic Options"
}
]
About this extraction
This page contains the full source code of the j0lv3r4/next-csrf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (43.1 KB), approximately 11.9k tokens, and a symbol index with 14 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.