Skip to content

Commit

Permalink
Merge pull request #40 from j0lv3r4/setup-integration-testing
Browse files Browse the repository at this point in the history
Prep for release 0.2.0
  • Loading branch information
j0lvera authored Apr 23, 2022
2 parents a273b77 + 5946f98 commit 45d5fce
Show file tree
Hide file tree
Showing 39 changed files with 8,917 additions and 25,972 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@ dist

.now
example/.vercel

#### yalc ####
example/.yalc
**/.yalc/**/*.md
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Juan Olvera
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
Expand Down
121 changes: 74 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# 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
## Installation

With yarn:

Expand All @@ -24,77 +24,104 @@ With npm:
npm i next-csrf --save
```

### Usage
## Usage

Setup:
Create an initialization file to add options:

```js
// file: lib/csrf.js

import { nextCsrf } from "next-csrf";

const options = {
secret: process.env.CSRF_SECRET // Long, randomly-generated, unique, and unpredictable value
}
const { csrf, setup } = nextCsrf({
// eslint-disable-next-line no-undef
secret: process.env.CSRF_SECRET,
});

export const { csrf, csrfToken } = nextCsrf(options);
```
export { csrf, setup };

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.
```

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.
Protect an API endpoint:

You can pass the token down as a prop on a custom `_app.js` and then use it on your first request.
```js
// file: pages/api/protected.js

Keep in mind that the token is valid only on the first request, since we create a new one on each request.
import { csrf } from '../lib/csrf';

Custom App:
const handler = (req, res) => {
return res.status(200).json({ message: "This API route is protected."})
}

```js
// file: pages/_app.js
import App from 'next/app'
import { csrfToken } from '../lib/csrf';
export default csrf(handler);
```

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} csrfToken={csrfToken} />
}
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.

export default MyApp
```shell
curl -X POST http://localhost:3000/api/protected
>> {"message": "Invalid CSRF token"}
```

Usage with `fetch`:
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.

```jsx
function Login({ csrfToken }) {
const sendRequest = async (e) => {
e.preventDefault();
```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', {
'headers': {
'XSRF-TOKEN': csrfToken,
}
method: 'post'
});
// ...
};


if (response.ok) {
console.log('protected response ok');
}
}

return (
<Form onSubmit={sendRequest}>
// ...
</Form>
);
<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;
```

Protect an API endpoint:
## API

```js
// file: pages/api/protected.js
### `nextCsrf(options);`

import { csrf } from '../lib/csrf';
Returns two functions:

const handler = (req, res) => {
return res.status(200).json({ message: "This API route is protected."})
}
* `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.

export default csrf(handler);
```
#### `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
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"
2 changes: 2 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vercel
.yalc
40 changes: 40 additions & 0 deletions example/components/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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;
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 { csrf, setup } = nextCsrf({
// eslint-disable-next-line no-undef
secret: process.env.CSRF_SECRET,
});

export { csrf, setup };
Loading

0 comments on commit 45d5fce

Please sign in to comment.