@nhz.io/slush-jwt-auth-proxy-conf
v1.0.1
Published
CouchDB JWT Auth Proxy nginx.conf generator
Readme
CouchDB JWT Auth Proxy nginx.conf generator
Nginx (openresty) configuration generator to act as JWT Proxy Authentication Gate for CouchDB.
Install
npm i -g slush @nhz.io/slush-jwt-auth-proxy-confUsage
mkdir jwt-auth-proxy && cd jwt-auth-proxy
slush @nhz.io/slush-jwt-auth-proxy-confSecrets
JWT_SECRET- key used to sign and verify JWT withCOUCH_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
usernameandroleswhich will be proxied in headers to CouchDB - Invalid JWT results in HTTP 403
JWT
JWT Payload example:
{
"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-Authheader - 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
PATHwill 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 = trueGenerated files
nginx.conf- openresty configurationpackage.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-confagain - You can distribute the
package.jsonand regeneratenginx.confanywhere by runningnpm 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 gulpString 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 = trueDefaults
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
returnServer configuration
gulp.task '_server', -> try answers = await inquirer.prompt serverPrompts
gulp.task 'server', (cb) -> sequence 'load-pkg', '_server', '_regenerate', cbProxy configuration
gulp.task '_proxy', -> try answers = await inquirer.prompt proxyPrompts
gulp.task 'proxy', (cb) -> sequence 'load-pkg', '_proxy', '_regenerate', cbRoutes 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', cbMain 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', cbRegenerate 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