@nhz.io/slush-jwt-auth-proxy-conf
Version:
CouchDB JWT Auth Proxy nginx.conf generator
504 lines (431 loc) • 12.8 kB
JavaScript
// # CouchDB JWT Auth Proxy nginx.conf generator
// > Nginx ([openresty]) configuration generator to act as [JWT] [Proxy Authentication] Gate for [CouchDB].
// ## Install
// ```bash
// npm i -g slush @nhz.io/slush-jwt-auth-proxy-conf
// ```
// ## Usage
// ```bash
// mkdir jwt-auth-proxy && cd jwt-auth-proxy
// slush @nhz.io/slush-jwt-auth-proxy-conf
// ```
// ## Secrets
// * `JWT_SECRET` - key used to sign and verify JWT with
// * `COUCH_PROXY_SECRET` - key used to generate [x_auth_token]
// ## Effect
// * JWT Auth Proxy will process requests and proxy them to [CouchDB]
// * Requests are authorized by verifying signature and expiration of JWT
// * JWT comes either from headers, url
// * JWT will always be cached in the cookie
// * JWT carries payload which contains `username` and `roles` which will be proxied in headers to [CouchDB]
// * Invalid JWT results in **HTTP 403**
// ## JWT
// JWT Payload example:
// ```json
// {
// "exp": 1510356100,
// "iat": 1510356220,
// "data": {
// "user": "boss",
// "roles": ["_admin"]
// }
// }
// ```
// JWT with such payload will grant admin access to **boss** user for 120 seconds
// ### JWT From headers (Low priority)
// * JWT be extracted from `X-JWT-Auth` header
// * Request will be proxied to [CouchDB] as is
// * JWT will be cached in the cookie
// ### JWT From URL (High priority)
// Process URLs of form: `http://` `HOST` : `PORT` `/` `${TOKEN_PREFIX}` `JWT` `PATH`
// * Extract JWT from URL
// * `PATH` will be proxied to [CouchDB]
// * JWT will be cached in the cookie
// ## CouchDB configuration
// Make sure `local.ini` contains:
// ```
// authentication_handlers = {couch_httpd_auth, proxy_authentication_handler}
// proxy_use_secret = true
// ```
// ### Generated files
// * `nginx.conf` - [openresty] configuration
// * `package.json` - configuration settings are stored here for later reconfiguration
// ### Notes:
// * Intended to run in [Docker]
// * JWT by URL is preferred method (rather than headers)
// * You can use JWT by URL as a key to open session, (JWT in cookie) and rest of requests with basename `/`
// * You can revisit the configuration later by running `slush @nhz.io/slush-jwt-auth-proxy-conf` again
// * You can distribute the `package.json` and regenerate `nginx.conf` anywhere by running `npm i`
// * Use [jwt-hs256-proxy-auth-token] to generate tokens
// ## Imports
// > Builtins
var addRoutePropmts, def, editRoutesPrompt, gulp, inquirer, map, newConfigurationPrompt, path, pkg, proxyPrompts, pump, reconfigurationPrompt, regen, removeRoutesPrompt, rename, routesPrompt, sequence, serverPrompts, slugify, template, transform, viewRoutesPrompt;
path = require('path');
// > General
gulp = require('gulp');
pump = require('pump');
inquirer = require('inquirer');
transform = require('vinyl-transform');
map = require('map-stream');
// > Gulp plugins
rename = require('gulp-rename');
template = require('gulp-template');
sequence = (require('run-sequence')).use(gulp);
// > String utils imports
slugify = require('slugify');
// > Global package.json variable (Corresponds to current directory)
pkg = (function() {
try {
return require('./package.json');
} catch (error) {}
})();
// > Global flag which marks regeneration run (will be cleared if no package.json found)
regen = true;
// ## Defaults
def = {
pkgName: 'nginx.conf',
pkgVersion: '1.0.0',
HOST: '',
PORT: 80,
COUCH_HOST: 'couch',
COUCH_PORT: 5984,
COUCH_SCHEMA: 'http',
// > This is the only secret here, base JWT token to use when there is none.
// > Could be used to setup default unpriviledged access.
// > MUST HAVE VERY LONG expiration
DEFAULT_JWT_TOKEN: '',
// > Those are not secrets, those are names of ENV variables
JWT_SECRET: 'JWT_SECRET',
COUCH_PROXY_SECRET: 'COUCH_PROXY_SECRET',
JWT_COOKIE_NAME: 'JWT',
JWT_HEADER_NAME: 'X-JWT-Auth',
JWT_TOKEN_PREFIX: '!',
ERROR_LOG: '/var/log/nginx/error.log warn',
PID_PATH: '/var/run/nginx.pid',
REWRITE: '^(/.*) $1',
ROLES: [],
defaultRoutes: {
'/': [],
'^/_': ['_admin'],
'^/_session': [],
'^/_users': []
}
};
// ## Prompts
// ### New configuration prompt
newConfigurationPrompt = {
name: 'task',
type: 'list',
message: 'JWT Auth Proxy configuration',
choices: ['Create nginx.conf', 'Done']
};
// ### Reconfiguration prompt
reconfigurationPrompt = {
name: 'task',
type: 'list',
message: 'JWT Auth Proxy configuration',
choices: [
'Configure Server',
'Configure Proxy',
// > Route editing is unfinished, so disabled for now
// 'Configure Routes'
'Regenerate',
'Done'
]
};
// ### Route configuration prompts
routesPrompt = {
name: 'task',
type: 'list',
message: 'JWT Auth Proxy Route configuration',
newChoices: ['Add Route', 'Done'],
reconfigureChoices: ['Add Route', 'Remove Routes', 'Edit Routes', 'View Routes', 'Done']
};
removeRoutesPrompt = {
name: 'routes',
type: 'checkbox',
message: 'Select routes to remove',
choices: ['Done']
};
editRoutesPrompt = {
name: 'routes',
type: 'list',
message: 'Select route to edit',
choices: ['Done']
};
viewRoutesPrompt = {
name: 'routes',
type: 'list',
message: 'Select route to edit',
choices: ['Done']
};
addRoutePropmts = [
{
name: 'MATCH',
message: 'Match regexp',
validate: true
},
{
name: 'ROLES',
message: 'Required roles (comma separated)',
default: []
},
{
name: 'REWRITE',
message: 'Rewrite rule',
default: '^(/.*) $1'
}
];
// ### Server configuration prompts
serverPrompts = [
{
name: 'HOST',
message: 'JWT Proxy Auth server host',
default: (pkg.server || def).HOST
},
{
name: 'PORT',
message: 'JWT Proxy Auth server port',
default: (pkg.server || def).PORT
},
{
name: 'COUCH_HOST',
message: 'CouchDB host to proxy',
default: (pkg.server || def).COUCH_HOST
},
{
name: 'COUCH_PORT',
message: 'CouchDB port to proxy',
default: (pkg.server || def).COUCH_PORT
},
{
name: 'COUCH_SCHEMA',
message: 'CouchDB protocol schema',
default: (pkg.server || def).COUCH_SCHEMA
},
{
name: 'ERROR_LOG',
message: 'Error log path and level',
default: (pkg.server || def).ERROR_LOG
},
{
name: 'PID_PATH',
message: 'Nginx pid file path',
default: (pkg.server || def).PID_PATH
}
];
// ### Proxy Auth configuration prompts
proxyPrompts = [
{
name: 'JWT_SECRET',
message: 'JWT Secret ENV Variable name',
default: (pkg.proxy || def).JWT_SECRET
},
{
name: 'COUCH_PROXY_SECRET',
message: 'CouchDB Proxy Auth secret ENV Variable name',
default: (pkg.proxy || def).COUCH_PROXY_SECRET
},
{
name: 'JWT_COOKIE_NAME',
message: 'JWT token cookie name',
default: (pkg.proxy || def).JWT_COOKIE_NAME
},
{
name: 'JWT_HEADER_NAME',
message: 'JWT header name',
default: (pkg.proxy || def).JWT_HEADER_NAME
},
{
name: 'JWT_TOKEN_PREFIX',
message: 'JWT token prefix',
default: (pkg.proxy || def).JWT_TOKEN_PREFIX
}
];
// ## Tasks
// ### Package preloader
gulp.task('load-pkg', function() {
pkg = (function() {
try {
return require(path.resolve(process.cwd(), 'package.json'));
} catch (error) {
return regen = false;
}
})();
pkg = Object.assign({}, def, pkg);
});
// ### Server configuration
gulp.task('_server', async function() {
var answers;
try {
return answers = (await inquirer.prompt(serverPrompts));
} catch (error) {}
});
gulp.task('server', function(cb) {
return sequence('load-pkg', '_server', '_regenerate', cb);
});
// ### Proxy configuration
gulp.task('_proxy', async function() {
var answers;
try {
return answers = (await inquirer.prompt(proxyPrompts));
} catch (error) {}
});
gulp.task('proxy', function(cb) {
return sequence('load-pkg', '_proxy', '_regenerate', cb);
});
// ### Routes configuration menu
gulp.task('_view-routes', async function() {
var anwsers;
return anwsers = (await inquirer.prompt([viewRoutesPrompt]));
});
gulp.task('view-routes', function() {
return sequence('load-pkg', '_view-routes');
});
gulp.task('_edit-routes', async function() {
var answers;
return answers = (await inquirer.prompt([editRoutesPrompt]));
});
gulp.task('edit-routes', function(cb) {
return sequence('load-pkg', '_edit-routes');
});
gulp.task('_delete-routes', async function() {
var answers;
return answers = (await inqurer.prompt([deleteRoutesPrompt]));
});
gulp.task('delete-routes', function() {
return sequence('load-pkg', '_delete-routes');
});
gulp.task('_routes', async function() {
var answers, prompt;
prompt = Object.assign({}, routesPrompt);
prompt.choices = regen ? prompt.reconfigureChoices : prompt.newChoices;
answers = (await inquirer.prompt([prompt]));
console.log(JSON.stringify(answers, null, 2));
return new Promise(function(res) {
switch (answers.task) {
case 'Add Route':
return sequence('_server', res);
case 'Remove Routes':
return sequence('_remove_routes', '_regenerate', '_routes', res);
case 'Edit Routes':
return sequence('_edit_routes', '_regenerate', '_routes', res);
case 'View Routes':
return sequence('_view_routes', '_regenerate', '_routes', res);
default:
return res();
}
});
});
gulp.task('routes', function(cb) {
return sequence('load-pkg', '_routes', cb);
});
// ### Main menu
gulp.task('_default', async function() {
var answers, prompt, res;
prompt = regen ? reconfigurationPrompt : newConfigurationPrompt;
while (true) {
answers = (await inquirer.prompt([prompt]));
res = (await new Promise(function(res) {
switch (answers.task) {
case 'Create nginx.conf':
return sequence('_server', '_proxy', '_regenerate', res);
case 'Configure Server':
return sequence('_server', '_regenerate', res);
case 'Configure Proxy':
return sequence('_proxy', '_regenerate', res);
case 'Configure Routes':
return sequence('_routes', '_regenerate', res);
case 'Regenerate':
return sequence('_regenerate', res);
default:
return res('exit');
}
}));
if (res === 'exit') {
return;
}
prompt = reconfigurationPrompt;
}
});
gulp.task('default', function(cb) {
return sequence('load-pkg', '_default', cb);
});
// ### Regenerate *nginx.conf* and *package.json*
gulp.task('_regenerate', async function() {
var context, defaultRoutes, locations, proxy, routes, server;
server = pkg.server || {};
proxy = pkg.proxy || {};
defaultRoutes = pkg.defaultRoutes;
routes = Object.assign({}, defaultRoutes, pkg.routes || {});
// > Remap routes into consumable form
routes = (Object.keys(routes)).map(function(MATCH) {
var value;
value = routes[MATCH];
if (typeof value === 'string') {
return {
MATCH,
REWRITE: value
};
}
if (Array.isArray(value)) {
return {
MATCH,
ROLES: value
};
}
return {
MATCH,
REWRITE: (value != null ? value.rewrite : void 0) || '',
ROLES: (value != null ? value.roles : void 0) || []
};
});
// > Generate locations from routes fixing rewrite and roles
locations = routes.map(function({MATCH, REWRITE, ROLES}) {
// > Transform rewrite rule or use default
REWRITE = REWRITE ? REWRITE.replace(/^(.+) +(.+)/, '$1 /rewrite$2') : '^(/.*) /rewrite$1';
// > Transform roles
ROLES = ROLES.join(', ');
return {MATCH, REWRITE, ROLES};
});
// > Create template context
context = Object.assign({}, pkg, server, proxy, {locations, routes});
// > JWT_COOKIE_NAME and JWT_HEADER_NAME need *snake_case* version
context.JWT_COOKIE_NAME_snake_case = (slugify(context.JWT_COOKIE_NAME, '_')).toLowerCase();
context.JWT_HEADER_NAME_snake_case = (slugify(context.JWT_HEADER_NAME, '_')).toLowerCase();
console.log(JSON.stringify(context, null, 2));
await pump([
gulp.src(__dirname + '/templates/**'),
(template(context)).on('error',
function(err) {
return console.log('OMG',
err);
}),
rename(function(f) {
if (f.basename[0] === '_') {
return f.basename = `.${f.basename.slice(1)}`;
}
}),
// > Prettify package.json
transform(function(filename) {
return map(function(chunk,
next) {
if (filename.match('package.json')) {
return next(null,
JSON.stringify(JSON.parse(chunk),
null,
2));
} else {
return next(null,
chunk);
}
});
}),
gulp.dest('./')
]);
});
gulp.task('regenerate', function(cb) {
return sequence('load-pkg', '_regenerate', cb);
});
// ## Version 1.0.1
// ## License [MIT](LICENSE)