Skip to content
This repository has been archived by the owner on Apr 21, 2020. It is now read-only.

Commit

Permalink
Version 1.5.9 multiple changes:
Browse files Browse the repository at this point in the history
    - Fix process.env var name in readme. It's OAUTH_REVERSE_PROXY_CONFIG_DIR not ()_PATH.
    - Use path.join()
    - Bump version to 1.5.9
    - Minor: indents and readme
    - Require config files to end in '.json'
    - Reject dot files in any keystore.
    - Config files can be xml, case insensitive
    - More "Getting Started" info in readme
    - Support relative paths in config vars
    - Let logger format error stacks itself.
    - Better support for relative path names.
    - Use snake_case for local var names.
    - Undo log format changes (using format strings again).
    - Add test resources for abnormal config filenames.
    - Add cases to test that ProxyManager does not load disallowed filenames
    - !fixup
    - More snake_case
    - Fix indents
    - Fix test should syntax
  • Loading branch information
theopak committed Sep 2, 2015
1 parent 3315635 commit c52747d
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ reports
nuget-packages/
*.suo
bin/
debug/
debug/
*.log
22 changes: 11 additions & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
language: node_js
before_script:
- #sudo curl -LO http://xrl.us/cpanm
- #sudo chmod +x cpanm
- #sudo ./cpanm Net::OAuth || true
- cd test/clients/ruby
- bundle install
- cd ../../../
- sudo pip install requests-oauth
- npm install -g bunyan
- npm install -g grunt grunt-cli
- #sudo curl -LO http://xrl.us/cpanm
- #sudo chmod +x cpanm
- #sudo ./cpanm Net::OAuth || true
- cd test/clients/ruby
- bundle install
- cd ../../../
- sudo pip install requests-oauth
- npm install -g bunyan
- npm install -g grunt grunt-cli
script:
- grunt benchmark || true
- npm run coveralls
- grunt benchmark || true
- npm run coveralls
deploy:
provider: npm
email: ryan@ryanbreen.com
Expand Down
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
var path = require('path');
var oauth_reverse_proxy = require('./lib');

/**
Expand All @@ -13,6 +14,7 @@ if (!config_dir) {
config_dir = "/etc/oauth_reverse_proxy.d/";
}
}
config_dir = path.resolve(config_dir);

/**
* The logging directory can be provided as an environment variable. If not provided,
Expand All @@ -27,6 +29,7 @@ if (!log_dir) {
log_dir = "/var/log/oauth_reverse_proxy/";
}
}
log_dir = path.resolve(log_dir);

try {
var logger = require('./lib/logger.js').setLogDir(log_dir);
Expand All @@ -40,7 +43,7 @@ oauth_reverse_proxy.init(config_dir, function(err, proxy) {
// If we caught a fatal error creating the proxies, log it and pause briefly before
// exiting to give Bunyan a chance to flush this error message.
if (err) {
logger.fatal("Failed to create proxy due to %s:\n", err, err.stack);
logger.fatal("Failed to create proxy:\n", err, err.stack);
setTimeout(function() {
process.exit(2);
}, 2000);
Expand Down
2 changes: 1 addition & 1 deletion lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports.setLogDir = function(dir) {
default_config.streams.push({
"level": process.env.OAUTH_REVERSE_PROXY_LOG_LEVEL || "info",
"type": "rotating-file",
"path": dir + path.sep + "proxy.log"
"path": path.join(dir, "proxy.log")
});

default_logger = bunyan.createLogger(default_config);
Expand Down
9 changes: 8 additions & 1 deletion lib/proxy/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ ProxyKeystore.prototype.load = function(cb) {

var key_count = 0;

this_obj.oauth_secret_dir = path.resolve(this_obj.oauth_secret_dir);

fs.readdir(this_obj.oauth_secret_dir, function(err, list) {
if (err) {
logger.error("Failed to read key directory %s", this_obj.oauth_secret_dir);
Expand All @@ -55,8 +57,13 @@ ProxyKeystore.prototype.load = function(cb) {
// total number of key files will never be huge, we handle all of these as synchronous
// operations and return via callback once all files have been read.
list.forEach(function(file_name) {

// Reject dotfiles
// TODO (@theopak): Write unit tests for this.
if (file_name[0] === '.') return;

try {
var file_path = this_obj.oauth_secret_dir + path.sep + file_name;
var file_path = path.join(this_obj.oauth_secret_dir, file_name);
var stat = fs.statSync(file_path);

// For code-coverage purposes, ignore this check. I'm not sure how to test a file
Expand Down
24 changes: 19 additions & 5 deletions lib/proxy_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Object.defineProperty(module.exports, 'proxies', { value: {}, enumerable: true }
module.exports.init = function(config_dir, cb) {
if (!config_dir) throw new Error("config_directory invalid");

CONFIG_DIR = config_dir;
CONFIG_DIR = path.resolve(config_dir);

var stat = fs.statSync(CONFIG_DIR);

Expand Down Expand Up @@ -160,20 +160,34 @@ function loadConfigFiles(cb) {
stop_me[proxy_config_file] = proxy;
});

// TODO (@theopak): Consider rewriting this to make it recursive.
fs.readdir(CONFIG_DIR, function(err, files) {
/* istanbul ignore if */
if (err) return cb(err);

// Require a '.json' or '.xml' file ending and reject dotfiles
// TODO (@theopak): Write unit tests for this.
var config_files = [];
for (var i in files) {
if (path.basename(files[i])[0] === '.') {
continue;
} else if (!/^.*\.(json|xml)$/i.test(files[i])) {
continue;
} else {
config_files.push(files[i]);
}
}

// Create a callback that must be invoked once per file before actually firing. This callback
// is responsible for stopping and starting all proxies that have changed state.
var wrapped_cb = _.after(files.length, create_proxy_config_finalizer(stop_me, start_me, cb));
var wrapped_cb = _.after(config_files.length, create_proxy_config_finalizer(stop_me, start_me, cb));

// If there are no pending files, call wrapped_cb immediately.
if (files.length === 0) return wrapped_cb();
if (config_files.length === 0) return wrapped_cb();

files.forEach(function(file) {
config_files.forEach(function(file) {
logger.info('Loading proxy configuration file %s', file);
fs.readFile(CONFIG_DIR + path.sep + file, {'encoding':'utf8'}, function(err, data) {
fs.readFile(path.resolve(CONFIG_DIR, file), {'encoding':'utf8'}, function(err, data) {

try {
// Parse the configuration into an object, create a ProxyConfig around it, and validate
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "oauth_reverse_proxy",
"description": "An OAuth 1.0a authenticating proxy and reverse proxy",
"version": "1.5.8",
"version": "1.5.9",
"contributors": [
{
"name": "Ryan Breen",
Expand Down
40 changes: 34 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,43 @@

[![Build Status](https://travis-ci.org/Cimpress-MCP/oauth_reverse_proxy.svg?branch=master)](https://travis-ci.org/Cimpress-MCP/oauth_reverse_proxy) [![Coverage Status](https://img.shields.io/coveralls/Cimpress-MCP/oauth_reverse_proxy.svg)](https://coveralls.io/r/Cimpress-MCP/oauth_reverse_proxy?branch=master)

oauth_[|reverse_]proxy is both a proxy that can sign outbound traffic as well as a reverse proxy capable of enforcing that callers present the correct OAuth credentials.
Layer to add authentication to APIs by checking caller credentials, reverse-proxying inbound traffic to your API, and then signing outbound traffic back to callers.

##### Motivation

Authenticaton for web applications, particularly applications created for machine-to-machine use, is often an afterthought or implemented in an insecure or incompatible fashion. We want a robust implementation of OAuth that can run on Windows or Unix systems in front of any HTTP-serving application and support clients written in any language. These are two-party connections, so we can use the simplest form of OAuth: zero-legged OAuth 1.0a.
Authentication for web applications, particularly applications created for machine-to-machine use, is often an afterthought or implemented in an insecure or incompatible fashion. We want a robust implementation of OAuth that can run on Windows or Unix systems in front of any HTTP-serving application and support clients written in any language. These are two-party connections, so we can use the simplest form of OAuth: zero-legged OAuth 1.0a.

##### Installation

`npm install oauth_reverse_proxy`
Since this project is published with [npm](https://www.npmjs.com), the installation and run commands are the same on Windows, OS X, and Linux. Here's a full bash example that includes configuration:

```bash
# Install the versioned node package from the public npm repo
$ npm install oauth_reverse_proxy

# Make a config file for each API
# NOTE default config dir on linux is '/etc/oauth_reverse_proxy.d/'
$ ls $OAUTH_REVERSE_PROXY_CONFIG_DIR
api_1.json api_2.json

# Make a directory of keys for each API, and generate keys
# NOTE the location of keystore directories comes from api configuration files
$ sudo mkdir /etc/api_1_keystore # api_1.json configured to use this dir
$ uuidgen | sudo tee /etc/api_1_keystore/example_key
$ sudo mkdir /etc/api_2_keystore # api_2.json configured to use this dir
$ uuidgen | sudo tee /etc/api_2_keystore/example_key

# Run the application
$ npm start

# Optional: run the application in PM2 instead, which makes it a system service
$ npm install -g pm2
$ pm2 start index.js --name "oauth_reverse_proxy" --no-daemon

# Optional: view proxy logs
# NOTE default log dir on linux is '/var/log/oauth_reverse_proxy/proxy.log'
$ cat $OAUTH_REVERSE_PROXY_LOG_DIR/proxy.log
```

##### Description

Expand All @@ -32,7 +60,7 @@ Zero-legged OAuth 1.0a is built on the assumption that a service provider can se

##### Configuration

`oauth_[|reverse_]proxy` looks for configuration files in either the location specified in the `OAUTH_REVERSE_PROXY_CONFIG_PATH` environment variable or in a sane default location (on Unix, that's `/etc/oauth_reverse_proxy.d`, on Windows, it's `C:\ProgramData\oauth_reverse_proxy\config.d\`). Each json file in that directory will be treated as the description of a proxy to run. Config files are only loaded on start. Invalid proxy config files are ignored and logged; they do not cause a total failure of `oauth_[|reverse_]proxy`.
`oauth_[|reverse_]proxy` looks for configuration files in either the location specified in the `OAUTH_REVERSE_PROXY_CONFIG_DIR` environment variable or in a sane default location (on Unix, that's `/etc/oauth_reverse_proxy.d`, on Windows, it's `C:\ProgramData\oauth_reverse_proxy\config.d\`). Each json file in that directory will be treated as the description of a proxy to run. Config files are only loaded on start. Invalid proxy config files are ignored and logged; they do not cause a total failure of `oauth_[|reverse_]proxy`.

###### Configuration Format

Expand Down Expand Up @@ -70,7 +98,7 @@ Zero-legged OAuth 1.0a is built on the assumption that a service provider can se
}
}

The following fields are required in a proxy configuration file:
Proxy configuration files can be JSON or XML. The following fields are required in a proxy configuration file:

- **service_name** - The name of the service for which we are proxying. This is used in logging to disambiguate messages for multiple proxies running within the same process.
- **from_port** - The port this proxy will open to the outside world. In the case of a reverse proxy, all inbound traffic to your service should be directed to this port to ensure that only authenticated requests reach your application. Note that only one proxy can be bound to any given `from_port`.
Expand All @@ -92,7 +120,7 @@ The following fields are optional for a proxy or reverse proxy:
- **required_hosts** (optional) - Sometimes you may have a situation where `oauth_[|reverse_]proxy` is sitting in front of another reverse proxy that is deferring to different systems based on the `Host` header. In these cases, you may wish to configure your proxy to only allow access to the routes that match a host in this list. This is to prevent client applications from authenticating against your proxy but accessing hosts that shouldn't be accessible by this proxy. The entries in `require_hosts` must exactly match the `Host` header of the inbound request, or the request will be rejected.
- **whitelist** (optional) - Sometimes you might want certain routes to be accessible without authentication. For example, if you expose a health check route to an upstream load balancer, it's unlikely that the load balancer will be able to authenticate those requests. In these cases, you can whitelist those specific routes that should not require authentication, and `oauth_[|reverse_]proxy` will pass any matching request through to your application.
- Whitelist is an array of config objects, each defining a path regex and a set of methods. For a request to be considered valid, it must match both components. For example, a `path` of "/livecheck" and a `methods` array containing only "GET" would whitelist any `GET` request against the URL path `/livecheck`. Keep in mind that the regex is interpreted as being between `^` and `$`, so the entire path must match this regex. A request for `/livecheck/test/a` would be rejected. If either path or method are omitted, it is assumed that all paths or methods match.
- **quotas** (optional) - The default behavior of `oauth_[|reverse_]proxy` is to allow an unlimited number of requests per key, but sometimes you want to constrain the volume of requests that can be made by consumers. The quotas object lets you define thresholds for an allowable volume of hits per key per unit time.
- **quotas** (optional) - The default behavior of `oauth_[|reverse_]proxy` is to allow an unlimited number of requests per key, but sometimes you want to constrain the volume of requests that can be made by consumers. The quotas object lets you define thresholds for an allowable volume of hits per key per unit time.
- `interval` specifies the time interval for which quotas apply: an interval of 1 means our quotas are hits-per-second while an interval of 60 specifies hits-per-minute.
- The `default_threshold` parameter gives us a catch-all for any key that is not given a specific threshold. If undefined, keys that lack specific thresholds are allowed to make an unbounded number of requests. In the example above, keys lacking defined thresholds are allowed to make 10 requests per minute.
- The `thresholds` array contains 0 or more mappings from a consumer key name to the acceptable threshold for that key. In the example above, the consumer_key "privileged_key" is allowed to make 1000 requests per second while "unprivileged_key" can only make 1 request per minute.
Expand Down
30 changes: 26 additions & 4 deletions test/abnormal_config_test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
var fs = require('fs');
var path = require('path');
var should = require('should');
var _ = require('underscore');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');

var oauth_reverse_proxy = require('../lib');
var Proxy = require('../lib/proxy');
var ProxyManager = require('../lib/proxy_manager.js');
var ProxyConfig = require('../lib/proxy/config.js');

// Start every test with an empty keys directory.
Expand Down Expand Up @@ -33,17 +35,37 @@ describe('basic config validation', function() {

it ('should reject an attempt to init oauth_reverse_proxy with an unset config_dir parameter', function() {
(function() { oauth_reverse_proxy.init(null, function() {}) }).
should.throw('config_directory invalid');
should.throw('config_directory invalid');
});

it ('should reject an attempt to init oauth_reverse_proxy with a config_dir referencing a nonexistent directory', function() {
(function() { oauth_reverse_proxy.init('./test/keys', function() {}) }).
should.throw("ENOENT, no such file or directory './test/keys'");
should.throw("ENOENT, no such file or directory './test/keys'");
});

it ('should reject an attempt to init oauth_reverse_proxy with a config_dir referencing a non-directory inode', function() {
(function() { oauth_reverse_proxy.init('./test/abnormal_config_test.js', function() {}) }).
should.throw("oauth_reverse_proxy config dir is not a directory");
should.throw("oauth_reverse_proxy config dir is not a directory");
});

it ('should not load any config file that has a filename beginning with a dot', function(done) {
var config_dir = './test/config.d/';
var config = JSON.parse(fs.readFileSync(path.join(config_dir, '.dotfile.json'), {'encoding': 'utf8'}));
var pm = ProxyManager;
pm.init('./test/config.d', function() {
pm.proxies.should.not.containDeep(config.service_name);
done();
});
});

it ('should not load any config file that has a filename not ending in \'json\' or \'xml\'', function(done) {
var config_dir = './test/config.d/';
var config = JSON.parse(fs.readFileSync(path.join(config_dir, 'invalid_file_extension.md'), {'encoding': 'utf8'}));
var pm = ProxyManager;
pm.init('./test/config.d', function() {
pm.proxies.should.not.containDeep(config.service_name);
done();
});
});
});

Expand Down Expand Up @@ -94,7 +116,7 @@ describe('detailed config validation', function() {
{ 'filename': 'to_port_on_client_proxy_service.json', 'expected_error': 'proxy configuration has a to_port and shouldn\'t'}
].forEach(function(validation) {
it ('should reject a proxy config with error: ' + validation.expected_error, function() {
var config_json = JSON.parse(fs.readFileSync('./test/config.d/' + validation.filename, {'encoding':'utf8'}));
var config_json = JSON.parse(fs.readFileSync(path.join('./test/config.d/', validation.filename), {'encoding': 'utf8'}));
var proxy_config = new ProxyConfig(config_json);
proxy_config.isInvalid().should.equal(validation.expected_error);
});
Expand Down
3 changes: 3 additions & 0 deletions test/config.d/.dotfile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"service_name": "invalid_config_filename_beginning_with_dot"
}
3 changes: 3 additions & 0 deletions test/config.d/invalid_file_extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"service_name": "invalid_config_filename_ending_other_than_json_or_xml"
}
2 changes: 1 addition & 1 deletion utils/keygen.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ exports.createKey = function(root_dir, from_port, to_port, key_id, secret, cb) {
/* istanbul ignore if */
if (err) return cb(err);

var keyfile = keystore_path + path.sep + key_id;
var keyfile = path.join(keystore_path, key_id);
fs.writeFile(keyfile, secret, function(err) {
return cb(err);
});
Expand Down

0 comments on commit c52747d

Please sign in to comment.