From 8e10b7da37e5f679f0a855ffb88265d12a2f2312 Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 22 May 2020 11:28:30 -0500 Subject: [PATCH 01/11] Add UI tab for specifying OpenID Connect options for proxy hosts. --- frontend/js/app/nginx/proxy/form.ejs | 49 ++++++++++++++++++++++++++++ frontend/js/app/nginx/proxy/form.js | 39 +++++++++++++++++----- frontend/js/models/proxy-host.js | 6 ++++ 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 1a4983013..3777b2591 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -11,6 +11,7 @@ +
@@ -270,6 +271,54 @@
+ + +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index 1dfb5c189..413712b3d 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -43,7 +43,9 @@ module.exports = Mn.View.extend({ dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', propagation_seconds: 'input[name="meta[propagation_seconds]"]', forward_scheme: 'select[name="forward_scheme"]', - letsencrypt: '.letsencrypt' + letsencrypt: '.letsencrypt', + openidc_enabled: 'input[name="openidc_enabled"]', + openidc: '.openidc' }, regions: { @@ -113,7 +115,7 @@ module.exports = Mn.View.extend({ } else { this.ui.dns_provider.prop('required', false); this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); + this.ui.dns_challenge_content.hide(); } }, @@ -125,13 +127,24 @@ module.exports = Mn.View.extend({ this.ui.credentials_file_content.show(); } else { this.ui.dns_provider_credentials.prop('required', false); - this.ui.credentials_file_content.hide(); + this.ui.credentials_file_content.hide(); + } + }, + + 'change @ui.openidc_enabled': function () { + console.log('Changing'); + let checked = this.ui.openidc_enabled.prop('checked'); + + if (checked) { + this.ui.openidc.show().find('input').prop('required', true); + } else { + this.ui.openidc.hide().find('input').prop('required', false); } }, 'click @ui.add_location_btn': function (e) { e.preventDefault(); - + const model = new ProxyLocationModel.Model(); this.locationsCollection.add(model); }, @@ -167,17 +180,18 @@ module.exports = Mn.View.extend({ data.hsts_enabled = !!data.hsts_enabled; data.hsts_subdomains = !!data.hsts_subdomains; data.ssl_forced = !!data.ssl_forced; - + data.openidc_enabled = data.openidc_enabled === '1'; + if (typeof data.meta === 'undefined') data.meta = {}; data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; data.meta.dns_challenge = data.meta.dns_challenge == 1; - + if(!data.meta.dns_challenge){ data.meta.dns_provider = undefined; data.meta.dns_provider_credentials = undefined; data.meta.propagation_seconds = undefined; } else { - if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; + if(data.meta.propagation_seconds === '') data.meta.propagation_seconds = undefined; } if (typeof data.domain_names === 'string' && data.domain_names) { @@ -185,7 +199,7 @@ module.exports = Mn.View.extend({ } // Check for any domain names containing wildcards, which are not allowed with letsencrypt - if (data.certificate_id === 'new') { + if (data.certificate_id === 'new') { let domain_err = false; if (!data.meta.dns_challenge) { data.domain_names.map(function (name) { @@ -203,6 +217,12 @@ module.exports = Mn.View.extend({ data.certificate_id = parseInt(data.certificate_id, 10); } + // OpenID Connect won't work with multiple domain names because the redirect URL has to point to a specific one + if (data.openidc_enabled && data.domain_names.length > 1) { + alert('Cannot use mutliple domain names when OpenID Connect is enabled'); + return; + } + let method = App.Api.Nginx.ProxyHosts.create; let is_new = true; @@ -344,6 +364,9 @@ module.exports = Mn.View.extend({ view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id')); } }); + + // OpenID Connect + this.ui.openidc.hide().find('input').prop('required', false); }, initialize: function (options) { diff --git a/frontend/js/models/proxy-host.js b/frontend/js/models/proxy-host.js index b82d09fef..77302a7d6 100644 --- a/frontend/js/models/proxy-host.js +++ b/frontend/js/models/proxy-host.js @@ -22,6 +22,12 @@ const model = Backbone.Model.extend({ block_exploits: false, http2_support: false, advanced_config: '', + openidc_enabled: false, + openidc_redirect_uri: null, + openidc_discovery: null, + openidc_auth_method: null, + openidc_client_id: null, + openidc_client_secret: null, enabled: true, meta: {}, // The following are expansions: From 53792a5cf700987a20c05c8ae83be0cdd48d9d10 Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 22 May 2020 12:31:03 -0500 Subject: [PATCH 02/11] Add database columns to store OpenID Connect information for Proxy Hosts. --- .../20200522113248_openid_connect.js | 48 +++++++++++++ backend/schema/definitions.json | 21 ++++++ backend/schema/endpoints/proxy-hosts.json | 72 +++++++++++++++++++ frontend/js/app/nginx/proxy/form.ejs | 2 +- frontend/js/app/nginx/proxy/form.js | 2 +- frontend/js/models/proxy-host.js | 10 +-- 6 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 backend/migrations/20200522113248_openid_connect.js diff --git a/backend/migrations/20200522113248_openid_connect.js b/backend/migrations/20200522113248_openid_connect.js new file mode 100644 index 000000000..f27a5e62d --- /dev/null +++ b/backend/migrations/20200522113248_openid_connect.js @@ -0,0 +1,48 @@ +const migrate_name = 'openid_connect'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.integer('openidc_enabled').notNull().unsigned().defaultTo(0); + proxy_host.text('openidc_redirect_uri').notNull().defaultTo(''); + proxy_host.text('openidc_discovery').notNull().defaultTo(''); + proxy_host.text('openidc_auth_method').notNull().defaultTo('client_secret_post'); + proxy_host.text('openidc_client_id').notNull().defaultTo(''); + proxy_host.text('openidc_client_secret').notNull().defaultTo(''); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex/*, Promise*/) { + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.dropColumn('openidc_enabled'); + proxy_host.dropColumn('openidc_redirect_uri'); + proxy_host.dropColumn('openidc_discovery'); + proxy_host.dropColumn('openidc_auth_method'); + proxy_host.dropColumn('openidc_client_id'); + proxy_host.dropColumn('openidc_client_secret'); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); +}; diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json index 9895b87ee..87db39a85 100644 --- a/backend/schema/definitions.json +++ b/backend/schema/definitions.json @@ -235,6 +235,27 @@ "description": "Should we cache assets", "example": true, "type": "boolean" + }, + "openidc_enabled": { + "description": "Is OpenID Connect authentication enabled", + "example": true, + "type": "boolean" + }, + "openidc_redirect_uri": { + "type": "string" + }, + "openidc_discovery": { + "type": "string" + }, + "openidc_auth_method": { + "type": "string", + "pattern": "^(client_secret_basic|client_secret_post)$" + }, + "openidc_client_id": { + "type": "string" + }, + "openidc_client_secret": { + "type": "string" } } } diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json index 9a3fff2fc..849a0f76d 100644 --- a/backend/schema/endpoints/proxy-hosts.json +++ b/backend/schema/endpoints/proxy-hosts.json @@ -64,6 +64,24 @@ "advanced_config": { "type": "string" }, + "openidc_enabled": { + "$ref": "../definitions.json#/definitions/openidc_enabled" + }, + "openidc_redirect_uri": { + "$ref": "../definitions.json#/definitions/openidc_redirect_uri" + }, + "openidc_discovery": { + "$ref": "../definitions.json#/definitions/openidc_discovery" + }, + "openidc_auth_method": { + "$ref": "../definitions.json#/definitions/openidc_auth_method" + }, + "openidc_client_id": { + "$ref": "../definitions.json#/definitions/openidc_client_id" + }, + "openidc_client_secret": { + "$ref": "../definitions.json#/definitions/openidc_client_secret" + }, "enabled": { "$ref": "../definitions.json#/definitions/enabled" }, @@ -161,6 +179,24 @@ "advanced_config": { "$ref": "#/definitions/advanced_config" }, + "openidc_enabled": { + "$ref": "#/definitions/openidc_enabled" + }, + "openidc_redirect_uri": { + "$ref": "#/definitions/openidc_redirect_uri" + }, + "openidc_discovery": { + "$ref": "#/definitions/openidc_discovery" + }, + "openidc_auth_method": { + "$ref": "#/definitions/openidc_auth_method" + }, + "openidc_client_id": { + "$ref": "#/definitions/openidc_client_id" + }, + "openidc_client_secret": { + "$ref": "#/definitions/openidc_client_secret" + }, "enabled": { "$ref": "#/definitions/enabled" }, @@ -251,6 +287,24 @@ "advanced_config": { "$ref": "#/definitions/advanced_config" }, + "openidc_enabled": { + "$ref": "#/definitions/openidc_enabled" + }, + "openidc_redirect_uri": { + "$ref": "#/definitions/openidc_redirect_uri" + }, + "openidc_discovery": { + "$ref": "#/definitions/openidc_discovery" + }, + "openidc_auth_method": { + "$ref": "#/definitions/openidc_auth_method" + }, + "openidc_client_id": { + "$ref": "#/definitions/openidc_client_id" + }, + "openidc_client_secret": { + "$ref": "#/definitions/openidc_client_secret" + }, "enabled": { "$ref": "#/definitions/enabled" }, @@ -324,6 +378,24 @@ "advanced_config": { "$ref": "#/definitions/advanced_config" }, + "openidc_enabled": { + "$ref": "#/definitions/openidc_enabled" + }, + "openidc_redirect_uri": { + "$ref": "#/definitions/openidc_redirect_uri" + }, + "openidc_discovery": { + "$ref": "#/definitions/openidc_discovery" + }, + "openidc_auth_method": { + "$ref": "#/definitions/openidc_auth_method" + }, + "openidc_client_id": { + "$ref": "#/definitions/openidc_client_id" + }, + "openidc_client_secret": { + "$ref": "#/definitions/openidc_client_secret" + }, "enabled": { "$ref": "#/definitions/enabled" }, diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 3777b2591..36b62bb73 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -278,7 +278,7 @@
diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index 413712b3d..b72457a4a 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -132,7 +132,6 @@ module.exports = Mn.View.extend({ }, 'change @ui.openidc_enabled': function () { - console.log('Changing'); let checked = this.ui.openidc_enabled.prop('checked'); if (checked) { @@ -367,6 +366,7 @@ module.exports = Mn.View.extend({ // OpenID Connect this.ui.openidc.hide().find('input').prop('required', false); + this.ui.openidc_enabled.trigger('change'); }, initialize: function (options) { diff --git a/frontend/js/models/proxy-host.js b/frontend/js/models/proxy-host.js index 77302a7d6..ef1f1f406 100644 --- a/frontend/js/models/proxy-host.js +++ b/frontend/js/models/proxy-host.js @@ -23,11 +23,11 @@ const model = Backbone.Model.extend({ http2_support: false, advanced_config: '', openidc_enabled: false, - openidc_redirect_uri: null, - openidc_discovery: null, - openidc_auth_method: null, - openidc_client_id: null, - openidc_client_secret: null, + openidc_redirect_uri: '', + openidc_discovery: '', + openidc_auth_method: 'client_secret_post', + openidc_client_id: '', + openidc_client_secret: '', enabled: true, meta: {}, // The following are expansions: From 58113450505e892f9551007d1195dc8dce8d50cf Mon Sep 17 00:00:00 2001 From: Subv Date: Mon, 25 May 2020 11:45:47 -0500 Subject: [PATCH 03/11] Use OpenResty instead of plain nginx to support OpenID Connect authorization. --- backend/templates/_openid_connect.conf | 26 ++++++++++++++++++++++++++ backend/templates/proxy_host.conf | 3 ++- docker/rootfs/etc/nginx/nginx.conf | 10 ++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 backend/templates/_openid_connect.conf diff --git a/backend/templates/_openid_connect.conf b/backend/templates/_openid_connect.conf new file mode 100644 index 000000000..9e0589c58 --- /dev/null +++ b/backend/templates/_openid_connect.conf @@ -0,0 +1,26 @@ +{% if openidc_enabled -%} + access_by_lua_block { + local openidc = require("resty.openidc") + local opts = { + redirect_uri = "{{- openidc_redirect_uri -}}", + discovery = "{{- openidc_discovery -}}", + token_endpoint_auth_method = "{{- openidc_auth_method -}}", + client_id = "{{- openidc_client_id -}}", + client_secret = "{{- openidc_client_secret -}}", + scope = "openid email profile" + } + + local res, err = openidc.authenticate(opts) + + if err then + ngx.status = 500 + ngx.say(err) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + end + + + ngx.req.set_header("X-OIDC-SUB", res.id_token.sub) + ngx.req.set_header("X-OIDC-EMAIL", res.id_token.email) + ngx.req.set_header("X-OIDC-NAME", res.id_token.name) + } +{% endif %} \ No newline at end of file diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index ec30cca0d..5629694e1 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -51,7 +51,8 @@ proxy_http_version 1.1; {% endif %} -{% include "_hsts.conf" %} + {% include "_openid_connect.conf" %} + {% include "_hsts.conf" %} {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} proxy_set_header Upgrade $http_upgrade; diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 4d5ee9017..0a58cdbf5 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -43,6 +43,16 @@ http { proxy_cache_path /var/lib/nginx/cache/public levels=1:2 keys_zone=public-cache:30m max_size=192m; proxy_cache_path /var/lib/nginx/cache/private levels=1:2 keys_zone=private-cache:5m max_size=1024m; + lua_package_path '~/lua/?.lua;;'; + + lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; + lua_ssl_verify_depth 5; + + # cache for discovery metadata documents + lua_shared_dict discovery 1m; + # cache for JWKs + lua_shared_dict jwks 1m; + log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"'; log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; From cdf702e5456c8d340bc03d689228993d178f78eb Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 22 May 2020 15:51:34 -0500 Subject: [PATCH 04/11] Add a field to specify a list of allowed emails when using OpenID Connect auth. --- .../20200522144240_openid_allowed_users.js | 40 +++++++++ backend/models/proxy_host.js | 13 ++- backend/schema/definitions.json | 16 ++++ backend/schema/endpoints/proxy-hosts.json | 24 +++++ frontend/js/app/nginx/proxy/form.ejs | 19 +++- frontend/js/app/nginx/proxy/form.js | 88 +++++++++++++------ frontend/js/models/proxy-host.js | 2 + 7 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 backend/migrations/20200522144240_openid_allowed_users.js diff --git a/backend/migrations/20200522144240_openid_allowed_users.js b/backend/migrations/20200522144240_openid_allowed_users.js new file mode 100644 index 000000000..9b4e0aad3 --- /dev/null +++ b/backend/migrations/20200522144240_openid_allowed_users.js @@ -0,0 +1,40 @@ +const migrate_name = 'openid_allowed_users'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.integer('openidc_restrict_users_enabled').notNull().unsigned().defaultTo(0); + proxy_host.json('openidc_allowed_users').notNull().defaultTo([]); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex/*, Promise*/) { + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.dropColumn('openidc_restrict_users_enabled'); + proxy_host.dropColumn('openidc_allowed_users'); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); +}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index a75830886..e86b802ec 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -20,12 +20,18 @@ class ProxyHost extends Model { this.domain_names = []; } + // Default for openidc_allowed_users + if (typeof this.openidc_allowed_users === 'undefined') { + this.openidc_allowed_users = []; + } + // Default for meta if (typeof this.meta === 'undefined') { this.meta = {}; } this.domain_names.sort(); + this.openidc_allowed_users.sort(); } $beforeUpdate () { @@ -35,6 +41,11 @@ class ProxyHost extends Model { if (typeof this.domain_names !== 'undefined') { this.domain_names.sort(); } + + // Sort openidc_allowed_users + if (typeof this.openidc_allowed_users !== 'undefined') { + this.openidc_allowed_users.sort(); + } } static get name () { @@ -46,7 +57,7 @@ class ProxyHost extends Model { } static get jsonAttributes () { - return ['domain_names', 'meta', 'locations']; + return ['domain_names', 'meta', 'locations', 'openidc_allowed_users']; } static get relationMappings () { diff --git a/backend/schema/definitions.json b/backend/schema/definitions.json index 87db39a85..eda360507 100644 --- a/backend/schema/definitions.json +++ b/backend/schema/definitions.json @@ -256,6 +256,22 @@ }, "openidc_client_secret": { "type": "string" + }, + "openidc_restrict_users_enabled": { + "description": "Only allow a specific set of OpenID Connect emails to access the resource", + "example": true, + "type": "boolean" + }, + "openidc_allowed_users": { + "type": "array", + "minItems": 0, + "items": { + "type": "string", + "description": "Email Address", + "example": "john@example.com", + "format": "email", + "minLength": 1 + } } } } diff --git a/backend/schema/endpoints/proxy-hosts.json b/backend/schema/endpoints/proxy-hosts.json index 849a0f76d..118622566 100644 --- a/backend/schema/endpoints/proxy-hosts.json +++ b/backend/schema/endpoints/proxy-hosts.json @@ -82,6 +82,12 @@ "openidc_client_secret": { "$ref": "../definitions.json#/definitions/openidc_client_secret" }, + "openidc_restrict_users_enabled": { + "$ref": "../definitions.json#/definitions/openidc_restrict_users_enabled" + }, + "openidc_allowed_users": { + "$ref": "../definitions.json#/definitions/openidc_allowed_users" + }, "enabled": { "$ref": "../definitions.json#/definitions/enabled" }, @@ -197,6 +203,12 @@ "openidc_client_secret": { "$ref": "#/definitions/openidc_client_secret" }, + "openidc_restrict_users_enabled": { + "$ref": "#/definitions/openidc_restrict_users_enabled" + }, + "openidc_allowed_users": { + "$ref": "#/definitions/openidc_allowed_users" + }, "enabled": { "$ref": "#/definitions/enabled" }, @@ -305,6 +317,12 @@ "openidc_client_secret": { "$ref": "#/definitions/openidc_client_secret" }, + "openidc_restrict_users_enabled": { + "$ref": "#/definitions/openidc_restrict_users_enabled" + }, + "openidc_allowed_users": { + "$ref": "#/definitions/openidc_allowed_users" + }, "enabled": { "$ref": "#/definitions/enabled" }, @@ -396,6 +414,12 @@ "openidc_client_secret": { "$ref": "#/definitions/openidc_client_secret" }, + "openidc_restrict_users_enabled": { + "$ref": "#/definitions/openidc_restrict_users_enabled" + }, + "openidc_allowed_users": { + "$ref": "#/definitions/openidc_allowed_users" + }, "enabled": { "$ref": "#/definitions/enabled" }, diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 36b62bb73..41acf6cbd 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -280,7 +280,7 @@
@@ -317,6 +317,23 @@ +
+
+
+ +
+
+
+
+ + +
+
+
diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index b72457a4a..16278ebe8 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -21,31 +21,34 @@ module.exports = Mn.View.extend({ locationsCollection: new ProxyLocationModel.Collection(), ui: { - form: 'form', - domain_names: 'input[name="domain_names"]', - forward_host: 'input[name="forward_host"]', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', - add_location_btn: 'button.add_location', - locations_container: '.locations_container', - le_error_info: '#le-error-info', - certificate_select: 'select[name="certificate_id"]', - access_list_select: 'select[name="access_list_id"]', - ssl_forced: 'input[name="ssl_forced"]', - hsts_enabled: 'input[name="hsts_enabled"]', - hsts_subdomains: 'input[name="hsts_subdomains"]', - http2_support: 'input[name="http2_support"]', - dns_challenge_switch: 'input[name="meta[dns_challenge]"]', - dns_challenge_content: '.dns-challenge', - dns_provider: 'select[name="meta[dns_provider]"]', - credentials_file_content: '.credentials-file-content', - dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', - propagation_seconds: 'input[name="meta[propagation_seconds]"]', - forward_scheme: 'select[name="forward_scheme"]', - letsencrypt: '.letsencrypt', - openidc_enabled: 'input[name="openidc_enabled"]', - openidc: '.openidc' + form: 'form', + domain_names: 'input[name="domain_names"]', + forward_host: 'input[name="forward_host"]', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save', + add_location_btn: 'button.add_location', + locations_container: '.locations_container', + le_error_info: '#le-error-info', + certificate_select: 'select[name="certificate_id"]', + access_list_select: 'select[name="access_list_id"]', + ssl_forced: 'input[name="ssl_forced"]', + hsts_enabled: 'input[name="hsts_enabled"]', + hsts_subdomains: 'input[name="hsts_subdomains"]', + http2_support: 'input[name="http2_support"]', + dns_challenge_switch: 'input[name="meta[dns_challenge]"]', + dns_challenge_content: '.dns-challenge', + dns_provider: 'select[name="meta[dns_provider]"]', + credentials_file_content: '.credentials-file-content', + dns_provider_credentials: 'textarea[name="meta[dns_provider_credentials]"]', + propagation_seconds: 'input[name="meta[propagation_seconds]"]', + forward_scheme: 'select[name="forward_scheme"]', + letsencrypt: '.letsencrypt', + openidc_enabled: 'input[name="openidc_enabled"]', + openidc_restrict_users_enabled: 'input[name="openidc_restrict_users_enabled"]', + openidc_allowed_users: 'input[name="openidc_allowed_users"]', + openidc: '.openidc', + openidc_users: '.openidc_users', }, regions: { @@ -135,9 +138,18 @@ module.exports = Mn.View.extend({ let checked = this.ui.openidc_enabled.prop('checked'); if (checked) { - this.ui.openidc.show().find('input').prop('required', true); + this.ui.openidc.show().find('input').prop('disabled', false); } else { - this.ui.openidc.hide().find('input').prop('required', false); + this.ui.openidc.hide().find('input').prop('disabled', true); + } + }, + + 'change @ui.openidc_restrict_users_enabled': function () { + let checked = this.ui.openidc_restrict_users_enabled.prop('checked'); + if (checked) { + this.ui.openidc_users.show().find('input').prop('disabled', false); + } else { + this.ui.openidc_users.hide().find('input').prop('disabled', true); } }, @@ -180,6 +192,13 @@ module.exports = Mn.View.extend({ data.hsts_subdomains = !!data.hsts_subdomains; data.ssl_forced = !!data.ssl_forced; data.openidc_enabled = data.openidc_enabled === '1'; + data.openidc_restrict_users_enabled = data.openidc_restrict_users_enabled === '1'; + + if (data.openidc_restrict_users_enabled) { + if (typeof data.openidc_allowed_users === 'string' && data.openidc_allowed_users) { + data.openidc_allowed_users = data.openidc_allowed_users.split(','); + } + } if (typeof data.meta === 'undefined') data.meta = {}; data.meta.letsencrypt_agree = data.meta.letsencrypt_agree == 1; @@ -365,8 +384,21 @@ module.exports = Mn.View.extend({ }); // OpenID Connect - this.ui.openidc.hide().find('input').prop('required', false); + this.ui.openidc_allowed_users.selectize({ + delimiter: ',', + persist: false, + maxOptions: 15, + create: function (input) { + return { + value: input, + text: input + }; + } + }); + this.ui.openidc.hide().find('input').prop('disabled', true); + this.ui.openidc_users.hide().find('input').prop('disabled', true); this.ui.openidc_enabled.trigger('change'); + this.ui.openidc_restrict_users_enabled.trigger('change'); }, initialize: function (options) { diff --git a/frontend/js/models/proxy-host.js b/frontend/js/models/proxy-host.js index ef1f1f406..85429d18d 100644 --- a/frontend/js/models/proxy-host.js +++ b/frontend/js/models/proxy-host.js @@ -28,6 +28,8 @@ const model = Backbone.Model.extend({ openidc_auth_method: 'client_secret_post', openidc_client_id: '', openidc_client_secret: '', + openidc_restrict_users_enabled: false, + openidc_allowed_users: [], enabled: true, meta: {}, // The following are expansions: From daf399163ccc700e9538aac6030d7c212e5ee611 Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 22 May 2020 17:01:22 -0500 Subject: [PATCH 05/11] Allow limiting OpenID Connect auth to a list of users. --- backend/templates/_openid_connect.conf | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/templates/_openid_connect.conf b/backend/templates/_openid_connect.conf index 9e0589c58..19aa606d2 100644 --- a/backend/templates/_openid_connect.conf +++ b/backend/templates/_openid_connect.conf @@ -18,6 +18,27 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end + {% if openidc_restrict_users_enabled -%} + local function contains(table, val) + for i=1,#table do + if table[i] == val then + return true + end + end + return false + end + + local allowed_users = { + {% for user in openidc_allowed_users %} + "{{ user }}", + {% endfor %} + } + + if not contains(allowed_users, res.id_token.email) then + ngx.exit(ngx.HTTP_FORBIDDEN) + end + {% endif -%} + ngx.req.set_header("X-OIDC-SUB", res.id_token.sub) ngx.req.set_header("X-OIDC-EMAIL", res.id_token.email) From 9f2d3a1737581237e45aacaba02346f4e939e251 Mon Sep 17 00:00:00 2001 From: Subv Date: Sat, 23 May 2020 11:46:28 -0500 Subject: [PATCH 06/11] Manually set the default values for the OpenID Connect columns. There is a Knex issue ( https://github.com/knex/knex/issues/2649 ) that prevents .defaultTo from working for text columns. --- .../20200523114256_openid_default_values.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 backend/migrations/20200523114256_openid_default_values.js diff --git a/backend/migrations/20200523114256_openid_default_values.js b/backend/migrations/20200523114256_openid_default_values.js new file mode 100644 index 000000000..90622edac --- /dev/null +++ b/backend/migrations/20200523114256_openid_default_values.js @@ -0,0 +1,36 @@ +const migrate_name = 'openid_default_values'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex/*, Promise*/) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_redirect_uri SET DEFAULT \'\'') + .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_discovery SET DEFAULT \'\'')) + .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_auth_method SET DEFAULT \'client_secret_post\'')) + .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_client_id SET DEFAULT \'\'')) + .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_client_secret SET DEFAULT \'\'')) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex, Promise) { + logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); + return Promise.resolve(true); +}; From 87d9babbd3382536ab1225a37256f7e36bc49845 Mon Sep 17 00:00:00 2001 From: Subv Date: Sat, 23 May 2020 12:55:47 -0500 Subject: [PATCH 07/11] Fix conditionals in the liquid template for OpenID Connect conf. --- backend/templates/_openid_connect.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/templates/_openid_connect.conf b/backend/templates/_openid_connect.conf index 19aa606d2..e5c2c7980 100644 --- a/backend/templates/_openid_connect.conf +++ b/backend/templates/_openid_connect.conf @@ -1,4 +1,4 @@ -{% if openidc_enabled -%} +{% if openidc_enabled == 1 or openidc_enabled == true -%} access_by_lua_block { local openidc = require("resty.openidc") local opts = { @@ -18,7 +18,7 @@ ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) end - {% if openidc_restrict_users_enabled -%} + {% if openidc_restrict_users_enabled == 1 or openidc_restrict_users_enabled == true -%} local function contains(table, val) for i=1,#table do if table[i] == val then From 8539930f89de609d96c5f5487886a60cdf253c40 Mon Sep 17 00:00:00 2001 From: Subv Date: Thu, 28 May 2020 00:31:51 -0500 Subject: [PATCH 08/11] Updated the docs to add a section about OpenID Connect --- docs/advanced-config/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/advanced-config/README.md b/docs/advanced-config/README.md index c7b51a846..202717834 100644 --- a/docs/advanced-config/README.md +++ b/docs/advanced-config/README.md @@ -172,3 +172,26 @@ value by specifying it as a Docker environment variable. The default if not spec X_FRAME_OPTIONS: "sameorigin" ... ``` + +## OpenID Connect SSO + +You can secure any of your proxy hosts with OpenID Connect authentication, providing SSO support from an identity provider like Azure AD or KeyCloak. OpenID Connect support is provided through the [`lua-resty-openidc`](https://github.com/zmartzone/lua-resty-openidc) library of [`OpenResty`](https://github.com/openresty/openresty). + +You will need a few things to get started with OpenID Connect: + +- A registered application with your identity provider, they will provide you with a `Client ID` and a `Client Secret`. Public OpenID Connect applications (without a client secret) are not yet supported. + +- A redirect URL to send the users to after they login with the identity provider, this can be any unused URL under the proxy host, like `https:///private/callback`, the server will take care of capturing that URL and redirecting you to the proxy host root. You will need to add this URL to the list of allowed redirect URLs for the application you registered with your identity provider. + +- The well-known discovery endpoint of the identity provider you want to use, this is an URL usually with the form `https:///.well-known/openid-configuration`. + +After you have all this you can proceed to configure the proxy host with OpenID Connect authentication. + +You can also add some rudimentary access control through a list of allowed emails in case your identity provider doesn't let you do that, if this option is enabled, any email not on that list will be denied access to the proxied host. + +The proxy adds some headers based on the authentication result from the identity provider: + + - `X-OIDC-SUB`: The subject identifier, according to the OpenID Coonect spec: `A locally unique and never reassigned identifier within the Issuer for the End-User`. + - `X-OIDC-EMAIL`: The email of the user that logged in, as specified in the `id_token` returned from the identity provider. The same value that will be checked for the email whitelist. + - `X-OIDC-NAME`: The user's name claim from the `id_token`, please note that not all id tokens necessarily contain this claim. + From 076d89b5b5e2e4e5fae9a02322d5245c58ecbc57 Mon Sep 17 00:00:00 2001 From: Subv Date: Fri, 29 May 2020 23:05:17 -0500 Subject: [PATCH 09/11] Use localized strings for the OpenID Connect texts. --- frontend/js/app/nginx/proxy/form.ejs | 18 +++++++++--------- frontend/js/i18n/messages.json | 11 ++++++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/js/app/nginx/proxy/form.ejs b/frontend/js/app/nginx/proxy/form.ejs index 41acf6cbd..6d8b484e4 100644 --- a/frontend/js/app/nginx/proxy/form.ejs +++ b/frontend/js/app/nginx/proxy/form.ejs @@ -11,7 +11,7 @@ - +
@@ -280,25 +280,25 @@
- +
- +
- +
- +
@@ -323,13 +323,13 @@
- +
diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index 9feb82d24..bfdc0725b 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -130,7 +130,16 @@ "access-list": "Access List", "allow-websocket-upgrade": "Websockets Support", "ignore-invalid-upstream-ssl": "Ignore Invalid SSL", - "custom-forward-host-help": "Use 1.1.1.1/path for sub-folder forwarding" + "custom-forward-host-help": "Use 1.1.1.1/path for sub-folder forwarding", + "oidc": "OpenID Connect", + "oidc-enabled": "Use OpenID Connect authentication", + "oidc-redirect-uri": "Redirect URI", + "oidc-discovery-endpoint": "Well-known discovery endpoint", + "oidc-token-auth-method": "Token endpoint auth method", + "oidc-client-id": "Client ID", + "oidc-client-secret": "Client secret", + "oidc-allow-only-emails": "Allow only these user emails", + "oidc-allowed-emails": "Allowed email addresses" }, "redirection-hosts": { "title": "Redirection Hosts", From e7f7be2a2bcd13f51b2900f685114e48f244d7c0 Mon Sep 17 00:00:00 2001 From: Subv Date: Tue, 2 Jun 2020 18:52:08 -0500 Subject: [PATCH 10/11] OpenIDC: Trigger the change event of the "restrict users" toggle when enabling/disabling oidc. If this is not triggered and the OIDC toggle is enabled, the "disabled" property will be removed from the restricted user list input, causing an error when trying to submit the form without it. --- frontend/js/app/nginx/proxy/form.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/js/app/nginx/proxy/form.js b/frontend/js/app/nginx/proxy/form.js index 16278ebe8..3a99c4c13 100644 --- a/frontend/js/app/nginx/proxy/form.js +++ b/frontend/js/app/nginx/proxy/form.js @@ -142,6 +142,8 @@ module.exports = Mn.View.extend({ } else { this.ui.openidc.hide().find('input').prop('disabled', true); } + + this.ui.openidc_restrict_users_enabled.trigger('change'); }, 'change @ui.openidc_restrict_users_enabled': function () { From a91dcb144d5b15daa35f1cbf048970a0587635fb Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Thu, 27 Aug 2020 10:10:31 +1000 Subject: [PATCH 11/11] Use model for db defaults as sqlite doesn't support them --- .../20200522113248_openid_connect.js | 2 +- .../20200523114256_openid_default_values.js | 36 ------------------- backend/models/proxy_host.js | 5 +++ 3 files changed, 6 insertions(+), 37 deletions(-) delete mode 100644 backend/migrations/20200523114256_openid_default_values.js diff --git a/backend/migrations/20200522113248_openid_connect.js b/backend/migrations/20200522113248_openid_connect.js index f27a5e62d..6054e1a11 100644 --- a/backend/migrations/20200522113248_openid_connect.js +++ b/backend/migrations/20200522113248_openid_connect.js @@ -17,7 +17,7 @@ exports.up = function (knex/*, Promise*/) { proxy_host.integer('openidc_enabled').notNull().unsigned().defaultTo(0); proxy_host.text('openidc_redirect_uri').notNull().defaultTo(''); proxy_host.text('openidc_discovery').notNull().defaultTo(''); - proxy_host.text('openidc_auth_method').notNull().defaultTo('client_secret_post'); + proxy_host.text('openidc_auth_method').notNull().defaultTo(''); proxy_host.text('openidc_client_id').notNull().defaultTo(''); proxy_host.text('openidc_client_secret').notNull().defaultTo(''); }) diff --git a/backend/migrations/20200523114256_openid_default_values.js b/backend/migrations/20200523114256_openid_default_values.js deleted file mode 100644 index 90622edac..000000000 --- a/backend/migrations/20200523114256_openid_default_values.js +++ /dev/null @@ -1,36 +0,0 @@ -const migrate_name = 'openid_default_values'; -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_redirect_uri SET DEFAULT \'\'') - .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_discovery SET DEFAULT \'\'')) - .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_auth_method SET DEFAULT \'client_secret_post\'')) - .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_client_id SET DEFAULT \'\'')) - .then(() => knex.schema.raw('ALTER TABLE proxy_host ALTER openidc_client_secret SET DEFAULT \'\'')) - .then(() => { - logger.info('[' + migrate_name + '] proxy_host Table altered'); - }); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); -}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index e86b802ec..30d4c73bc 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -30,6 +30,11 @@ class ProxyHost extends Model { this.meta = {}; } + // Openidc defaults + if (typeof this.openidc_auth_method === 'undefined') { + this.openidc_auth_method = 'client_secret_post'; + } + this.domain_names.sort(); this.openidc_allowed_users.sort(); }