Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add middleware for koa 2 #7

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url]

An electrode plugin that enables stateless CSRF protection using [JWT](https://github.com/auth0/node-jsonwebtoken) in Electrode, Express, or Hapi applications.
An electrode plugin that enables stateless CSRF protection using [JWT](https://github.com/auth0/node-jsonwebtoken) in Electrode, Express, Hapi, or Koa 2 applications.

## Why do we need this module?

Expand Down Expand Up @@ -55,7 +55,7 @@ Others are optional and follow the [same usage as jsonwebtoken](https://github.c
* `noTimestamp`
* `headers`

This module can be used with either [Electrode](#electrode), [Express](#express), or [Hapi](#hapi).
This module can be used with either [Electrode](#electrode), [Express](#express), [Hapi](#hapi), or [Koa 2](#koa-2).

### Electrode

Expand Down Expand Up @@ -113,6 +113,24 @@ server.register({register: csrfPlugin, options}, (err) => {
});
```

### Koa 2

#### Example `app.js` configuration

```js
const csrfMiddleware = require("electrode-csrf-jwt").koaMiddleware;
const Koa = require("koa");

const app = new Koa();

const options = {
secret: "shhhhh",
expiresIn: 60
};

app.use(csrfMiddleware(options));
```

Built with :heart: by [Team Electrode](https://github.com/orgs/electrode-io/people) @WalmartLabs.

[npm-image]: https://badge.fury.io/js/electrode-csrf-jwt.svg
Expand Down
61 changes: 61 additions & 0 deletions lib/csrf-koa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use strict";

const Promise = require("bluebird");
const uuid = require("uuid");
const csrf = require("./csrf");

const MISSING_SECRET = "MISSING_SECRET";
const INVALID_JWT = "INVALID_JWT";

function csrfMiddleware(options) {
if (!options || !options.secret) {
throw new Error(MISSING_SECRET);
}

function middleware(ctx, next) {

function createToken() {
const id = uuid.v4();
const headerPayload = {type: "header", uuid: id};
const cookiePayload = {type: "cookie", uuid: id};

return Promise.all([
csrf.create(headerPayload, options),
csrf.create(cookiePayload, options)
]).spread((headerToken, cookieToken) => {
ctx.set("x-csrf-jwt", headerToken);
ctx.cookies.set("x-csrf-jwt", cookieToken);
return next();
});
}

function verifyAndCreateToken() {
const headerPayload = {token: ctx.headers["x-csrf-jwt"], secret: options.secret};
const cookiePayload = {token: ctx.cookies.get("x-csrf-jwt"), secret: options.secret};

return Promise.all([
csrf.verify(headerPayload),
csrf.verify(cookiePayload)
]).spread((headerToken, cookieToken) => {
if (headerToken.uuid === cookieToken.uuid &&
headerToken.type === "header" && cookieToken.type === "cookie") {
return createToken();
}
ctx.throw(new Error(INVALID_JWT));
}).catch((err) => {
ctx.throw(err);
});
}

const method = ctx.method.toUpperCase();
if (method !== "GET" && method !== "HEAD") {
return verifyAndCreateToken();
}

return createToken();
}

return middleware;
}

module.exports = csrfMiddleware;
4 changes: 3 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

const csrfHapi = require("./csrf-hapi");
const csrfExpress = require("./csrf-express");
const csrfKoa = require("./csrf-koa");

module.exports = {
register: csrfHapi,
expressMiddleware: csrfExpress
expressMiddleware: csrfExpress,
koaMiddleware: csrfKoa
};
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"lint": "gulp lint",
"test": "npm run lint && gulp test",
"coverage": "istanbul cover -x lib/csrf-express.js node_modules/.bin/_mocha",
"coverage": "istanbul cover -x lib/csrf-express.js lib/csrf-koa.js node_modules/.bin/_mocha",
"prepublish": "npm test"
},
"repository": {
Expand Down Expand Up @@ -41,6 +41,9 @@
"hapi": "^14.2.0",
"isomorphic-fetch": "^2.2.1",
"istanbul": "^0.4.5",
"koa": "^2.0.0",
"koa-bodyparser": "^3.2.0",
"koa-router": "^7.0.1",
"mocha": "^3.0.2",
"vision": "^4.0.1"
}
Expand Down
130 changes: 130 additions & 0 deletions test/spec/csrf-koa-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"use strict";

const bodyParser = require("koa-bodyparser");
const Router = require("koa-router");
const Koa = require("koa");
const csrfMiddleware = require("../../lib/index").koaMiddleware;
const jwt = require("jsonwebtoken");

const fetch = require("isomorphic-fetch");

const secret = "test";
const url = "http://localhost:4000";
let server;

describe("test register", () => {
it("should fail with bad options", () => {
const app = new Koa();
try {
app.use(csrfMiddleware());
} catch (e) {
expect(e.message).to.equal("MISSING_SECRET");
}
});
});

describe("test csrf-jwt koa middleware", () => {
before(() => {
const app = new Koa();
const router = new Router();

app.use(bodyParser());

const options = {
secret,
expiresIn: "2d",
ignoreThisParam: "ignore"
};
app.use(csrfMiddleware(options));

router.get("/1", (ctx) => {
ctx.body = "valid";
});

router.post("/2", (ctx) => {
expect(ctx.request.body.message).to.equal("hello");
ctx.body = "valid";
});

app.use(router.routes());

server = require("http").createServer(app.callback());
server.listen(4000);
});

after(() => {
server.close();
});

it("should return success", () => {
return fetch(`${url}/1`)
.then((res) => {
expect(res.status).to.equal(200);
const csrfHeader = res.headers.get("x-csrf-jwt");
const csrfCookie = res.headers.get("set-cookie");
expect(csrfHeader).to.exist;
expect(csrfCookie).to.contain("x-csrf-jwt=");

return fetch(`${url}/2`, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"x-csrf-jwt": csrfHeader,
"Cookie": csrfCookie
},
body: JSON.stringify({message: "hello"})
}).then((res) => {
expect(res.status).to.equal(200);
expect(res.headers.get("x-csrf-jwt")).to.exist;
expect(res.headers.get("set-cookie")).to.contain("x-csrf-jwt=");
});
});
});

it("should return 500 for missing jwt", () => {
return fetch(`${url}/2`, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({message: "hello"})
}).then((res) => {
expect(res.status).to.equal(500);
expect(res.headers.get("x-csrf-jwt")).to.not.exist;
expect(res.headers.get("set-cookie")).to.not.exist;
});
});

it("should return 500 for invalid jwt", () => {
return fetch(`${url}/1`)
.then(() => {
const token = jwt.sign({uuid: "1"}, secret, {});
return fetch(`${url}/2`, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"x-csrf-jwt": token,
"Cookie": `x-csrf-jwt=${token}`
},
body: JSON.stringify({message: "hello"})
}).then((res) => {
expect(res.status).to.equal(500);
return fetch(`${url}/2`, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"x-csrf-jwt": "invalid",
"Cookie": `x-csrf-jwt=${token}`
},
body: JSON.stringify({message: "hello"})
}).then((res) => {
expect(res.status).to.equal(500);
});
});
});
});
});