UNPKG

@nhz.io/slush-jwt-auth-proxy-conf

Version:
504 lines (431 loc) 12.8 kB
// # 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)