UNPKG

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

Version:
506 lines (358 loc) 12.8 kB
# CouchDB JWT Auth Proxy nginx.conf generator [![Travis Build][travis]](https://travis-ci.org/nhz-io/nhz-io-slush-jwt-auth-proxy-conf) [![NPM Version][npm]](https://www.npmjs.com/package/@nhz.io/slush-jwt-auth-proxy-conf) > 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: ``` [chttpd] authentication_handlers = {couch_httpd_auth, proxy_authentication_handler} [couch_httpd_auth] 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 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 = try require './package.json' > 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 or def).HOST } { name: 'PORT' message: 'JWT Proxy Auth server port' default: (pkg.server or def).PORT } { name: 'COUCH_HOST' message: 'CouchDB host to proxy' default: (pkg.server or def).COUCH_HOST } { name: 'COUCH_PORT' message: 'CouchDB port to proxy' default: (pkg.server or def).COUCH_PORT } { name: 'COUCH_SCHEMA' message: 'CouchDB protocol schema' default: (pkg.server or def).COUCH_SCHEMA } { name: 'ERROR_LOG' message: 'Error log path and level' default: (pkg.server or def).ERROR_LOG } { name: 'PID_PATH' message: 'Nginx pid file path' default: (pkg.server or def).PID_PATH } ] ### Proxy Auth configuration prompts proxyPrompts = [ { name: 'JWT_SECRET' message: 'JWT Secret ENV Variable name' default: (pkg.proxy or def).JWT_SECRET } { name: 'COUCH_PROXY_SECRET' message: 'CouchDB Proxy Auth secret ENV Variable name' default: (pkg.proxy or def).COUCH_PROXY_SECRET } { name: 'JWT_COOKIE_NAME' message: 'JWT token cookie name' default: (pkg.proxy or def).JWT_COOKIE_NAME } { name: 'JWT_HEADER_NAME' message: 'JWT header name' default: (pkg.proxy or def).JWT_HEADER_NAME } { name: 'JWT_TOKEN_PREFIX' message: 'JWT token prefix' default: (pkg.proxy or def).JWT_TOKEN_PREFIX } ] ## Tasks ### Package preloader gulp.task 'load-pkg', -> pkg = try require (path.resolve process.cwd(), 'package.json') catch then regen = false pkg = Object.assign {}, def, pkg return ### Server configuration gulp.task '_server', -> try answers = await inquirer.prompt serverPrompts gulp.task 'server', (cb) -> sequence 'load-pkg', '_server', '_regenerate', cb ### Proxy configuration gulp.task '_proxy', -> try answers = await inquirer.prompt proxyPrompts gulp.task 'proxy', (cb) -> sequence 'load-pkg', '_proxy', '_regenerate', cb ### Routes configuration menu gulp.task '_view-routes', -> anwsers = await inquirer.prompt [viewRoutesPrompt] gulp.task 'view-routes', -> sequence 'load-pkg', '_view-routes' gulp.task '_edit-routes', -> answers = await inquirer.prompt [editRoutesPrompt] gulp.task 'edit-routes', (cb) -> sequence 'load-pkg', '_edit-routes' gulp.task '_delete-routes', -> answers = await inqurer.prompt [deleteRoutesPrompt] gulp.task 'delete-routes', -> sequence 'load-pkg', '_delete-routes' gulp.task '_routes', -> prompt = Object.assign {}, routesPrompt prompt.choices = if regen then prompt.reconfigureChoices else prompt.newChoices answers = await inquirer.prompt [prompt] console.log JSON.stringify answers, null, 2 new Promise (res) -> switch answers.task when 'Add Route' then sequence '_server', res when 'Remove Routes' then sequence '_remove_routes', '_regenerate', '_routes', res when 'Edit Routes' then sequence '_edit_routes', '_regenerate', '_routes', res when 'View Routes' then sequence '_view_routes', '_regenerate', '_routes', res else res() gulp.task 'routes', (cb) -> sequence 'load-pkg', '_routes', cb ### Main menu gulp.task '_default', -> prompt = if regen then reconfigurationPrompt else newConfigurationPrompt loop answers = await inquirer.prompt [prompt] res = await new Promise (res) -> switch answers.task when 'Create nginx.conf' then sequence '_server', '_proxy', '_regenerate', res when 'Configure Server' then sequence '_server', '_regenerate', res when 'Configure Proxy' then sequence '_proxy', '_regenerate', res when 'Configure Routes' then sequence '_routes', '_regenerate', res when 'Regenerate' then sequence '_regenerate', res else res 'exit' if res is 'exit' then return prompt = reconfigurationPrompt gulp.task 'default', (cb) -> sequence 'load-pkg', '_default', cb ### Regenerate *nginx.conf* and *package.json* gulp.task '_regenerate', -> server = pkg.server or {} proxy = pkg.proxy or {} defaultRoutes = pkg.defaultRoutes routes = Object.assign {}, defaultRoutes, (pkg.routes or {}) > Remap routes into consumable form routes = (Object.keys routes).map (MATCH) -> value = routes[MATCH] if typeof value is 'string' then return { MATCH, REWRITE: value } if Array.isArray value then return { MATCH, ROLES: value } { MATCH, REWRITE: value?.rewrite or '', ROLES: value?.roles or [] } > Generate locations from routes fixing rewrite and roles locations = routes.map ({MATCH, REWRITE, ROLES}) -> > Transform rewrite rule or use default REWRITE = if REWRITE then REWRITE.replace /^(.+) +(.+)/, '$1 /rewrite$2' else '^(/.*) /rewrite$1' > Transform roles ROLES = ROLES.join ', ' { 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', (err) -> console.log 'OMG', err rename (f) -> if f.basename[0] is '_' then f.basename = ".#{ f.basename.slice 1 }" > Prettify package.json transform (filename) -> map (chunk, next) -> if filename.match 'package.json' next null, JSON.stringify (JSON.parse chunk), null, 2 else next null, chunk gulp.dest './' ] return gulp.task 'regenerate', (cb) -> sequence 'load-pkg', '_regenerate', cb ## Version 1.0.1 ## License [MIT](LICENSE) [travis]: https://img.shields.io/travis/nhz-io/nhz-io-slush-jwt-auth-proxy-conf.svg?style=flat [npm]: https://img.shields.io/npm/v/@nhz.io/slush-jwt-auth-proxy-conf.svg?style=flat [x_auth_roles]: http://docs.couchdb.org/en/2.1.1/config/auth.html#couch_httpd_auth/x_auth_roles [x_auth_token]: http://docs.couchdb.org/en/2.1.1/config/auth.html#couch_httpd_auth/x_auth_token [Proxy Authentication]: http://docs.couchdb.org/en/2.1.1/api/server/authn.html#api-auth-proxy [openresty]: https://github.com/openresty/lua-nginx-module [jwt-hs256-proxy-auth-token]: https://github.com/nhz-io/nhz-io-jwt-hs256-proxy-auth-token [JWT]: https://github.com/auth0/node-jsonwebtoken [CouchDB]: https://github.com/apache/couchdb [Docker]: https://www.docker.com/