Skip to content

Commit

Permalink
Merge pull request #6 from j0lv3r4/bug-fixes-documentation
Browse files Browse the repository at this point in the history
Update functionality, docs, and bug fixes
  • Loading branch information
j0lvera authored Sep 22, 2021
2 parents 81d6f1f + 525b0e3 commit 7da5783
Show file tree
Hide file tree
Showing 23 changed files with 510 additions and 9,717 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ dist
.idea

.now
example/.vercel
example/.vercel
58 changes: 20 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# next-csrf

![Discord](https://discord.com/api/guilds/778076094112464926/widget.png)

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))
* [Double-submit cookie pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie)
* [Custom request headers](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers)

### Installation

Expand All @@ -28,6 +28,8 @@ npm i next-csrf --save

Setup:

Create an initialization file to add options:

```js
// file: lib/csrf.js
import { nextCsrf } from "next-csrf";
Expand All @@ -39,50 +41,28 @@ const options = {
export const { csrf, csrfToken } = nextCsrf(options);
```

When you initialize `nextCsrf` it will return the middleware, and a valid signed CSRF token. You can send it along with a custom header on your first request to a protected API route. Is not required, but recommended.
Create a setup endpoint:

If you don't send the given CSRF token on the first request one is set up on any first request you send to a protected API route.
```js
// file: pages/api/csrf/setup.js
import { setup } from "../../../lib/csrf";

You can pass the token down as a prop on a custom `_app.js` and then use it on your first request.
const handler = (req, res) => {
res.statusCode = 200;
res.json({ message: "CSRF token added to cookies" });
};

Keep in mind that the token is valid only on the first request, since we create a new one on each request.
export default setup(handler);
```

Custom App:
On the first request, or any time you want to set up the CSRF token, send a GET request to the setup endpoint, in this example `/api/csrf/setup`, and you will receive a cookie with the CSRF token on the response.

```js
// file: pages/_app.js
import App from 'next/app'
import { csrfToken } from '../lib/csrf';
const response = await fetch('/api/csrf/setup');

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} csrfToken={csrfToken} />
if (response.ok) {
console.log('CSRF token setup')
}

export default MyApp
```

Usage with `fetch`:

```jsx
function Login({ csrfToken }) {
const sendRequest = async (e) => {
e.preventDefault();
const response = await fetch('/api/protected', {
'headers': {
'XSRF-TOKEN': csrfToken,
}
});
// ...
};

return (
<Form onSubmit={sendRequest}>
// ...
</Form>
);
}

export default Login;
```

Protect an API endpoint:
Expand All @@ -98,3 +78,5 @@ const handler = (req, res) => {

export default csrf(handler);
```

Every time you hit a protected API route you will replace the token in your cookie with a new one.
1 change: 1 addition & 0 deletions example/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CSRF_SECRET="P*3NGEEaJV3yUGDJA9428EQRg!ad"
1 change: 1 addition & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vercel
8 changes: 8 additions & 0 deletions example/lib/csrf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { nextCsrf } from "next-csrf";

const options = {
// eslint-disable-next-line no-undef
secret: process.env.CSRF_SECRET,
};

export const { csrf, setup, csrfToken } = nextCsrf(options);
18 changes: 18 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"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",
"react": "16.13.1",
"react-dom": "16.13.1"
},
"devDependencies": {
"prettier": "^2.1.1"
}
}
7 changes: 7 additions & 0 deletions example/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}

export default MyApp;
8 changes: 8 additions & 0 deletions example/pages/api/csrf/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { setup } from "../../../lib/csrf";

const handler = (req, res) => {
res.statusCode = 200;
res.json({ message: "CSRF token added to cookies" });
};

export default setup(handler);
6 changes: 6 additions & 0 deletions example/pages/api/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default (req, res) => {
res.statusCode = 200;
res.json({ name: "John Doe" });
};
9 changes: 9 additions & 0 deletions example/pages/api/protected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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);
99 changes: 99 additions & 0 deletions example/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { setup } from "../lib/csrf";

export default function Home() {
// We send a request to setup the csrf token
// fetch("http://localhost:3000/api/csrf/setup")
// .then((response) => {
// console.log(response);
// if (response.ok) {
// console.log("response ok");
// console.log("csrf token setup correctly");
// // console.log("cookies", document.cookie);
// }
// })
// .catch((error) => console.error(error));

const requestWithToken = () =>
fetch("/api/protected", {
method: "post",
})
.then((response) => {
if (response.ok) {
console.log("protected response ok");
console.log(response);
}
})
.catch((error) => console.error(error));

return (
<div className={styles.container}>
<Head>
<title>Next CSRF</title>
<link rel="icon" href="/favicon.ico" />
</Head>

<main className={styles.main}>
<h1 className={styles.title}>Next CSRF</h1>

<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 with a valid CSRF token</h3>

<p>
Open the Web Console and click in the button below to see how a
valid request works.
</p>

<button className={styles.button} onClick={requestWithToken}>
With CSRF token
</button>
</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>
</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 const getServerSideProps = setup(async () => {
return { props: {} };
});
Binary file added example/public/favicon.ico
Binary file not shown.
4 changes: 4 additions & 0 deletions example/public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7da5783

Please sign in to comment.