Skip to content

Commit

Permalink
feat: added ctx.state._csrf, removed ctx.csrf, ctx._csrf, and ctx.res…
Browse files Browse the repository at this point in the history
…ponse.csrf (fixes #50)
  • Loading branch information
titanism committed Jul 2, 2022
1 parent 8265e53 commit db4df33
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 56 deletions.
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

> CSRF tokens for Koa
> **NOTE:** As of v5.0.0+ `ctx.csrf`, `ctx_csrf`, and `ctx.response.csrf` are removed – instead use `ctx.state._csrf`

## Table of Contents

* [Install](#install)
* [Usage](#usage)
* [Options](#options)
* [Open Source Contributor Requests](#open-source-contributor-requests)
* [Contributors](#contributors)
* [License](#license)

Expand Down Expand Up @@ -64,7 +65,7 @@ npm install koa-csrf
if (![ 'GET', 'POST' ].includes(ctx.method))
return next();
if (ctx.method === 'GET') {
ctx.body = ctx.csrf;
ctx.body = ctx.state._csrf;
return;
}
ctx.body = 'OK';
Expand All @@ -79,7 +80,7 @@ npm install koa-csrf

```jade
form(action='/register', method='POST')
input(type='hidden', name='_csrf', value=csrf)
input(type='hidden', name='_csrf', value=_csrf)
input(type='email', name='email', placeholder='Email')
input(type='password', name='password', placeholder='Password')
button(type='submit') Register
Expand All @@ -89,7 +90,7 @@ npm install koa-csrf

```ejs
<form action="/register" method="POST">
<input type="hidden" name="_csrf" value="<%= csrf %>" />
<input type="hidden" name="_csrf" value="<%= _csrf %>" />
<input type="email" name="email" placeholder="Email" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Register</button>
Expand All @@ -103,12 +104,7 @@ npm install koa-csrf
* `invalidTokenStatusCode` (Number) - defaults to `403`
* `excludedMethods` (Array) - defaults to `[ 'GET', 'HEAD', 'OPTIONS' ]`
* `disableQuery` (Boolean) - defaults to `false`


## Open Source Contributor Requests

* [ ] Existing methods from 1.x package added to 3.x
* [ ] Existing tests from 1.x package added to 3.x
* `ignoredPathGlobs` (Array) - defaults to an empty Array, but you can pass an Array of glob paths to ignore


## Contributors
Expand Down
64 changes: 24 additions & 40 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const csrf = require('csrf');
const isSANB = require('is-string-and-not-blank');
const multimatch = require('multimatch');

function CSRF(opts = {}) {
const tokens = csrf(opts);
Expand All @@ -8,65 +10,47 @@ function CSRF(opts = {}) {
invalidTokenStatusCode: 403,
excludedMethods: ['GET', 'HEAD', 'OPTIONS'],
disableQuery: false,
ignoredPathGlobs: [],
...opts
};

return function (ctx, next) {
Object.defineProperty(ctx, 'csrf', {
get() {
if (ctx._csrf) {
return ctx._csrf;
}
// eslint-disable-next-line complexity
return async function (ctx, next) {
if (!ctx.session) return next();

if (!ctx.session) {
return null;
}
if (!ctx.session.secret) ctx.session.secret = await tokens.secret();

if (!ctx.session.secret) {
ctx.session.secret = tokens.secretSync();
}
if (!ctx.state._csrf) ctx.state._csrf = tokens.create(ctx.session.secret);

ctx._csrf = tokens.create(ctx.session.secret);
if (opts.excludedMethods.includes(ctx.method)) return next();

return ctx._csrf;
}
});

Object.defineProperty(ctx.response, 'csrf', {
get: () => ctx.csrf
});

if (opts.excludedMethods.includes(ctx.method)) {
return next();
// check against ignored/whitelisted redirect middleware paths
if (
Array.isArray(opts.ignoredPathGlobs) &&
opts.ignoredPathGlobs.length > 0
) {
const match = multimatch(ctx.path, opts.ignoredPathGlobs);
if (Array.isArray(match) && match.length > 0) return next();
}

if (!ctx.session.secret) {
ctx.session.secret = tokens.secretSync();
}
const bodyToken = isSANB(ctx.request.body._csrf)
? ctx.request.body._csrf
: false;

const bodyToken =
ctx.request.body && typeof ctx.request.body._csrf === 'string'
? ctx.request.body._csrf
const queryToken =
!bodyToken && !opts.disableQuery && ctx.query && isSANB(ctx.query._csrf)
? ctx.query._csrf
: false;

const token =
bodyToken ||
(!opts.disableQuery && ctx.query && ctx.query._csrf) ||
queryToken ||
ctx.get('csrf-token') ||
ctx.get('xsrf-token') ||
ctx.get('x-csrf-token') ||
ctx.get('x-xsrf-token');

if (!token) {
return ctx.throw(
opts.invalidTokenStatusCode,
typeof opts.invalidTokenMessage === 'function'
? opts.invalidTokenMessage(ctx)
: opts.invalidTokenMessage
);
}

if (!tokens.verify(ctx.session.secret, token)) {
if (!token || !tokens.verify(ctx.session.secret, token)) {
return ctx.throw(
opts.invalidTokenStatusCode,
typeof opts.invalidTokenMessage === 'function'
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@
}
],
"dependencies": {
"csrf": "^3.1.0"
"csrf": "^3.1.0",
"is-string-and-not-blank": "^0.0.2",
"multimatch": "5"
},
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"ava": "^4.3.0",
"cross-env": "^7.0.3",
"eslint": "^8.18.0",
"eslint": "^8.19.0",
"eslint-config-xo-lass": "^2.0.1",
"fixpack": "^4.0.0",
"husky": "^8.0.1",
Expand Down
18 changes: 14 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const test = require('ava');
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-generic-session');
const convert = require('koa-convert');
const supertest = require('supertest');

const CSRF = require('..');
Expand All @@ -11,7 +10,10 @@ const tokenRegExp = /^\w+-[\w+/-]+/;

test.before((t) => {
t.context.request = getApp();
t.context.requestWithOpts = getApp({ disableQuery: true });
t.context.requestWithOpts = getApp({
disableQuery: true,
ignoredPathGlobs: ['/beep']
});
});

test('should create a token', async (t) => {
Expand Down Expand Up @@ -94,16 +96,24 @@ test('should not respect the _csrf querystring given disableQuery=true', async (
t.is(res2.text, 'Invalid CSRF token');
});

test('should ignore CSRF validation when ignoredPathGlobs matches', async (t) => {
await t.context.requestWithOpts.get('/');
await t.context.requestWithOpts.post('/beep');
const res = await t.context.requestWithOpts.post('/boop');
t.is(res.status, 403);
t.is(res.text, 'Invalid CSRF token');
});

function getApp(opts = {}) {
const app = new Koa();
app.keys = ['a', 'b'];
app.use(convert(session()));
app.use(session());
app.use(bodyParser());
app.use(new CSRF(opts));
app.use((ctx, next) => {
if (!['GET', 'POST'].includes(ctx.method)) return next();
if (ctx.method === 'GET') {
ctx.body = ctx.csrf;
ctx.body = ctx.state._csrf;
return;
}

Expand Down

0 comments on commit db4df33

Please sign in to comment.