Skip to content

Commit

Permalink
Merge pull request #7 from theverything/feature/add-koa-middleware
Browse files Browse the repository at this point in the history
add middleware for koa 2
  • Loading branch information
caoyangs authored Jan 5, 2017
2 parents b84e3f3 + 15191f3 commit 41e5502
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 4 deletions.
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);
});
});
});
});
});

0 comments on commit 41e5502

Please sign in to comment.