Skip to content

Commit 94a4607

Browse files
Merge pull request #8 from tyler-johnson/dev
2 parents c9ae3ad + a5acd0a commit 94a4607

File tree

8 files changed

+164
-13
lines changed

8 files changed

+164
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
22
/index.js
3+
/test.js

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ notifications:
77
email: false
88
node_js:
99
- '6'
10-
before_install:
11-
- npm i -g npm@latest
1210
before_script:
1311
- npm run lint
1412
script:

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ build: index.js
66
index.js: src/index.js $(SRC)
77
$(BIN)/rollup $< -c > $@
88

9+
test.js: test/index.js $(SRC)
10+
$(BIN)/rollup $< -c > $@
11+
912
clean:
10-
rm -rf index.js
13+
rm -rf index.js test.js
1114

1215
.PHONY: build clean

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# couchdb-auth-proxy
1+
# CouchDB Auth Proxy
22

3-
[![npm](https://img.shields.io/npm/v/couchdb-auth-proxy.svg)](https://www.npmjs.com/package/couchdb-auth-proxy) [![David](https://img.shields.io/david/tyler-johnson/couchdb-auth-proxy.svg)](https://david-dm.org/tyler-johnson/couchdb-auth-proxy) [![Build Status](https://travis-ci.org/tyler-johnson/couchdb-auth-proxy.svg?branch=master)](https://travis-ci.org/tyler-johnson/couchdb-auth-proxy)
3+
[![npm](https://img.shields.io/npm/v/couchdb-auth-proxy.svg)](https://www.npmjs.com/package/couchdb-auth-proxy) [![Build Status](https://travis-ci.org/tyler-johnson/couchdb-auth-proxy.svg?branch=master)](https://travis-ci.org/tyler-johnson/couchdb-auth-proxy)
44

55
A Node.js HTTP reverse proxy library for quick and dirty CouchDB proxy authentication.
66

@@ -66,7 +66,7 @@ const server = http.createServer(couchdbProxy(function(req, res, next) {
6666
- `userCtxFn` (Function, *required*) - Method called on every request, with the request `req` and response `res` as arguments. This method should return a plain object with `name` and `roles` fields, representing the authenticated user. To run an async task, return a promise or pass a third argument `next` for a callback.
6767
- `options` (Object) - Options to configure the proxy.
6868
- `options.target` (String) - The URL of the CouchDB server to proxy to. This server must have [proxy authentication enabled](http://docs.couchdb.org/en/1.6.1/api/server/authn.html#proxy-authentication). Defaults to `http://localhost:5984`.
69-
- `options.secret` (String) - The [CouchDB secret](http://docs.couchdb.org/en/1.6.1/config/auth.html#couch_httpd_auth/secret) used to sign proxy tokens and cookies. This is very much an optional parameter and in general there is very little reason to use a secret. This is only absolutely required if `couch_httpd_auth/proxy_use_secret` is enabled on CouchDB.
69+
- `options.secret` (String) - The [CouchDB secret](http://docs.couchdb.org/en/1.6.1/config/auth.html#couch_httpd_auth/secret) used to sign proxy tokens and cookies. This is only required if `couch_httpd_auth/proxy_use_secret` is enabled on CouchDB (which is recommended).
7070
- `options.via` (String) - The name of the proxy to add to the `Via` header. This is so consumers of the HTTP API can tell that the request was directed through a proxy. This is optional and the `Via` header will be excluded when not provided.
7171
- `options.headerFields` (Object) - A map of custom header fields to use for the proxy. This should match what is declared in CouchDB `couch_httpd_auth` configuration, under `x_auth_roles`, `x_auth_token`, and `x_auth_username`. This is the default map:
7272
```json

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"scripts": {
1212
"lint": "eslint src/ test/",
1313
"build": "make clean && make",
14+
"test": "make test.js && node test.js",
1415
"prepublish": "npm run build",
1516
"autorelease": "autorelease pre && npm publish && autorelease post"
1617
},
@@ -29,9 +30,13 @@
2930
"babel-plugin-transform-es2015-parameters": "^6.11.4",
3031
"babel-plugin-transform-object-rest-spread": "^6.8.0",
3132
"eslint": "^3.2.2",
33+
"express": "^4.15.2",
3234
"rollup": "^0.34.3",
3335
"rollup-plugin-babel": "^2.6.1",
34-
"rollup-plugin-json": "^2.0.1"
36+
"rollup-plugin-json": "^2.0.1",
37+
"supertest": "^3.0.0",
38+
"tape": "^4.6.3",
39+
"tape-promise": "^2.0.1"
3540
},
3641
"keywords": [],
3742
"license": "MIT",

src/index.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {parse} from "url";
44
import httpProxy from "http-proxy";
55
import transformerProxy from "transformer-proxy";
66

7-
export default function(fn, opts={}) {
7+
export default function couchdbAuthProxy(fn, opts={}) {
88
if (typeof fn === "object") [opts,fn] = [fn,opts];
99

1010
let {
@@ -51,10 +51,12 @@ export default function(fn, opts={}) {
5151
// inject couchdb proxy headers into request
5252
const ctx = await confusedAsync(fn, null, [ req, res ]);
5353
if (ctx != null) {
54+
const { username, roles, token } = headerFields;
55+
cleanHeaders(req, [ username, roles, token ]);
5456
const n = typeof ctx.name === "string" ? ctx.name : "";
55-
req.headers[headerFields.username] = n;
56-
req.headers[headerFields.roles] = Array.isArray(ctx.roles) ? ctx.roles.join(",") : "";
57-
if (secret) req.headers[headerFields.token] = signRequest(n, secret);
57+
req.headers[username] = n;
58+
req.headers[roles] = Array.isArray(ctx.roles) ? ctx.roles.join(",") : "";
59+
if (secret) req.headers[token] = sign(n, secret);
5860
}
5961

6062
proxy.web(req, res);
@@ -66,9 +68,9 @@ export default function(fn, opts={}) {
6668
}
6769

6870
// couchdb proxy signed token
69-
function signRequest(user, secret) {
71+
const sign = couchdbAuthProxy.sign = function(user, secret) {
7072
return createHmac("sha1", secret).update(user).digest("hex");
71-
}
73+
};
7274

7375
// for methods that we don't know if they are callback or promise async
7476
function confusedAsync(fn, ctx, args=[]) {
@@ -83,3 +85,13 @@ function confusedAsync(fn, ctx, args=[]) {
8385
return Promise.resolve(fn.apply(ctx, args));
8486
}
8587
}
88+
89+
// removes a list of headers from a request
90+
// accounts for Node.js lowercase headers
91+
// https://github.com/tyler-johnson/couchdb-auth-proxy/issues/7
92+
function cleanHeaders(req, headers) {
93+
headers.forEach(header => {
94+
delete req.headers[header];
95+
delete req.headers[header.toLowerCase()];
96+
});
97+
}

test/index.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {test} from "./utils";
2+
import couchdbProxy from "../src/index";
3+
import request from "supertest";
4+
5+
test("attaches proxy headers", async function(t, couchdb, target) {
6+
t.plan(4);
7+
8+
couchdb.use(function(req, res) {
9+
t.equals(req.get("X-Auth-CouchDB-UserName"), "test", "proxied correct user name");
10+
t.equals(req.get("X-Auth-CouchDB-Roles"), "testrole1,testrole2", "proxied correct roles");
11+
t.notOk(req.get("X-Auth-CouchDB-Token"), "did not proxy a token");
12+
res.json({ foo: "bar" });
13+
});
14+
15+
const proxy = couchdbProxy(function() {
16+
return {
17+
name: "test",
18+
roles: [ "testrole1", "testrole2" ]
19+
};
20+
}, {
21+
target
22+
});
23+
24+
const {body} = await request(proxy).get("/").expect(200);
25+
t.equals(body.foo, "bar", "has original json content");
26+
});
27+
28+
test("signs request with secret", async function(t, couchdb, target) {
29+
t.plan(3);
30+
const secret = "mysupersecret";
31+
const token = couchdbProxy.sign("test", secret);
32+
33+
couchdb.use(function(req, res) {
34+
t.equals(req.get("X-Auth-CouchDB-Token"), token, "proxied signed token");
35+
res.json({ foo: "bar" });
36+
});
37+
38+
const proxy = couchdbProxy(function() {
39+
return {
40+
name: "test",
41+
roles: [ "testrole1", "testrole2" ]
42+
};
43+
}, {
44+
target, secret
45+
});
46+
47+
t.ok(token, "created a token");
48+
49+
const {body} = await request(proxy).get("/").expect(200);
50+
t.equals(body.foo, "bar", "has original json content");
51+
});
52+
53+
test("injects proxy info into root response", async function(t, couchdb, target) {
54+
t.plan(2);
55+
56+
couchdb.get("/", (req, res) => {
57+
res.json({ foo: "bar" });
58+
});
59+
60+
const proxy = couchdbProxy(function() {
61+
return {
62+
name: "test",
63+
roles: [ "testrole1", "testrole2" ]
64+
};
65+
}, {
66+
target,
67+
info: { hello: "world" }
68+
});
69+
70+
const {body} = await request(proxy).get("/").expect(200);
71+
t.equals(body.foo, "bar", "has original json content");
72+
t.deepEquals(body.proxy, { hello: "world" }, "has custom info contents");
73+
});
74+
75+
test("removes original session headers", async function(t, couchdb, target) {
76+
t.plan(3);
77+
78+
couchdb.get("/", (req, res) => {
79+
t.equals(req.get("X-Auth-CouchDB-UserName"), "test", "proxied correct user name");
80+
t.equals(req.get("X-Auth-CouchDB-Roles"), "testrole1,testrole2", "proxied correct roles");
81+
res.json({ foo: "bar" });
82+
});
83+
84+
const proxy = couchdbProxy(function() {
85+
return {
86+
name: "test",
87+
roles: [ "testrole1", "testrole2" ]
88+
};
89+
}, {
90+
target
91+
});
92+
93+
const {body} = await request(proxy)
94+
.get("/")
95+
.set("X-Auth-CouchDB-UserName", "previous")
96+
.set("X-Auth-CouchDB-Roles", "prevrole1,prevrole2")
97+
.expect(200);
98+
99+
t.equals(body.foo, "bar", "has original json content");
100+
});

test/utils.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import tape from "tape";
2+
import tapePromise from "tape-promise";
3+
import http from "http";
4+
import express from "express";
5+
6+
const tapeTest = tapePromise(tape);
7+
8+
export async function startServer(server) {
9+
await new Promise((resolve, reject) => {
10+
server.once("error", reject);
11+
server.listen(0, () => {
12+
server.removeListener("error", reject);
13+
resolve();
14+
});
15+
});
16+
17+
return "http://127.0.0.1:" + server.address().port;
18+
}
19+
20+
export function test(name, fn) {
21+
return tapeTest(name, async function(t) {
22+
const couchdb = express();
23+
const couchdbServer = http.createServer(couchdb);
24+
25+
try {
26+
const couchdbUrl = await startServer(couchdbServer);
27+
await fn.call(this, t, couchdb, couchdbUrl);
28+
} finally {
29+
couchdbServer.close();
30+
}
31+
});
32+
}

0 commit comments

Comments
 (0)