mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-18 10:06:26 +00:00
Compare commits
13 Commits
8b841176fa
...
openidc
Author | SHA1 | Date | |
---|---|---|---|
fdb22e467b | |||
694d8a0f21 | |||
a91dcb144d | |||
e7f7be2a2b | |||
076d89b5b5 | |||
8539930f89 | |||
87d9babbd3 | |||
9f2d3a1737 | |||
daf399163c | |||
cdf702e545 | |||
5811345050 | |||
53792a5cf7 | |||
8e10b7da37 |
@ -82,47 +82,6 @@ module.exports = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @param {String} data.identity
|
||||
* @param {String} [issuer]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
getTokenFromOAuthClaim: (data) => {
|
||||
let Token = new TokenModel();
|
||||
|
||||
data.scope = 'user';
|
||||
data.expiry = '1d';
|
||||
|
||||
return userModel
|
||||
.query()
|
||||
.where('email', data.identity)
|
||||
.andWhere('is_deleted', 0)
|
||||
.andWhere('is_disabled', 0)
|
||||
.first()
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
throw new error.AuthError('No relevant user found');
|
||||
}
|
||||
|
||||
// Create a moment of the expiry expression
|
||||
let expiry = helpers.parseDatePeriod(data.expiry);
|
||||
if (expiry === null) {
|
||||
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
|
||||
}
|
||||
|
||||
let iss = 'api',
|
||||
attrs = { id: user.id },
|
||||
scope = [ data.scope ],
|
||||
expiresIn = data.expiry;
|
||||
|
||||
return Token.create({ iss, attrs, scope, expiresIn })
|
||||
.then((signed) => {
|
||||
return { token: signed.token, expires: expiry.toISOString() };
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Access} access
|
||||
* @param {Object} [data]
|
||||
|
@ -4,9 +4,7 @@ module.exports = () => {
|
||||
return function (req, res, next) {
|
||||
res.locals.access = null;
|
||||
let access = new Access(res.locals.token || null);
|
||||
// allow unauthenticated access to OIDC configuration
|
||||
let anon_access = req.url === '/oidc-config' && !access.token.getUserId();
|
||||
access.load(anon_access)
|
||||
access.load()
|
||||
.then(() => {
|
||||
res.locals.access = access;
|
||||
next();
|
||||
|
@ -10,6 +10,5 @@ module.exports = {
|
||||
certbot: new Signale({scope: 'Certbot '}),
|
||||
import: new Signale({scope: 'Importer '}),
|
||||
setup: new Signale({scope: 'Setup '}),
|
||||
ip_ranges: new Signale({scope: 'IP Ranges'}),
|
||||
oidc: new Signale({scope: 'OIDC '})
|
||||
ip_ranges: new Signale({scope: 'IP Ranges'})
|
||||
};
|
||||
|
48
backend/migrations/20200522113248_openid_connect.js
Normal file
48
backend/migrations/20200522113248_openid_connect.js
Normal file
@ -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('');
|
||||
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');
|
||||
});
|
||||
};
|
40
backend/migrations/20200522144240_openid_allowed_users.js
Normal file
40
backend/migrations/20200522144240_openid_allowed_users.js
Normal file
@ -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');
|
||||
});
|
||||
};
|
@ -20,12 +20,23 @@ 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 = {};
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
$beforeUpdate () {
|
||||
@ -35,6 +46,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 +62,7 @@ class ProxyHost extends Model {
|
||||
}
|
||||
|
||||
static get jsonAttributes () {
|
||||
return ['domain_names', 'meta', 'locations'];
|
||||
return ['domain_names', 'meta', 'locations', 'openidc_allowed_users'];
|
||||
}
|
||||
|
||||
static get relationMappings () {
|
||||
|
@ -21,8 +21,6 @@
|
||||
"moment": "^2.29.4",
|
||||
"mysql": "^2.18.1",
|
||||
"node-rsa": "^1.0.8",
|
||||
"nodemon": "^2.0.2",
|
||||
"openid-client": "^5.4.0",
|
||||
"objection": "3.0.1",
|
||||
"path": "^0.12.7",
|
||||
"signale": "1.4.0",
|
||||
|
@ -27,7 +27,6 @@ router.get('/', (req, res/*, next*/) => {
|
||||
|
||||
router.use('/schema', require('./schema'));
|
||||
router.use('/tokens', require('./tokens'));
|
||||
router.use('/oidc', require('./oidc'));
|
||||
router.use('/users', require('./users'));
|
||||
router.use('/audit-log', require('./audit-log'));
|
||||
router.use('/reports', require('./reports'));
|
||||
|
@ -1,168 +0,0 @@
|
||||
const crypto = require('crypto');
|
||||
const error = require('../../lib/error');
|
||||
const express = require('express');
|
||||
const jwtdecode = require('../../lib/express/jwt-decode');
|
||||
const logger = require('../../logger').oidc;
|
||||
const oidc = require('openid-client');
|
||||
const settingModel = require('../../models/setting');
|
||||
const internalToken = require('../../internal/token');
|
||||
|
||||
let router = express.Router({
|
||||
caseSensitive: true,
|
||||
strict: true,
|
||||
mergeParams: true
|
||||
});
|
||||
|
||||
router
|
||||
.route('/')
|
||||
.options((req, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* GET /api/oidc
|
||||
*
|
||||
* OAuth Authorization Code flow initialisation
|
||||
*/
|
||||
.get(jwtdecode(), async (req, res) => {
|
||||
logger.info('Initializing OAuth flow');
|
||||
settingModel
|
||||
.query()
|
||||
.where({id: 'oidc-config'})
|
||||
.first()
|
||||
.then((row) => getInitParams(req, row))
|
||||
.then((params) => redirectToAuthorizationURL(res, params))
|
||||
.catch((err) => redirectWithError(res, err));
|
||||
});
|
||||
|
||||
|
||||
router
|
||||
.route('/callback')
|
||||
.options((req, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* GET /api/oidc/callback
|
||||
*
|
||||
* Oauth Authorization Code flow callback
|
||||
*/
|
||||
.get(jwtdecode(), async (req, res) => {
|
||||
logger.info('Processing callback');
|
||||
settingModel
|
||||
.query()
|
||||
.where({id: 'oidc-config'})
|
||||
.first()
|
||||
.then((settings) => validateCallback(req, settings))
|
||||
.then((token) => redirectWithJwtToken(res, token))
|
||||
.catch((err) => redirectWithError(res, err));
|
||||
});
|
||||
|
||||
/**
|
||||
* Executes discovery and returns the configured `openid-client` client
|
||||
*
|
||||
* @param {Setting} row
|
||||
* */
|
||||
let getClient = async (row) => {
|
||||
let issuer;
|
||||
try {
|
||||
issuer = await oidc.Issuer.discover(row.meta.issuerURL);
|
||||
} catch (err) {
|
||||
throw new error.AuthError(`Discovery failed for the specified URL with message: ${err.message}`);
|
||||
}
|
||||
|
||||
return new issuer.Client({
|
||||
client_id: row.meta.clientID,
|
||||
client_secret: row.meta.clientSecret,
|
||||
redirect_uris: [row.meta.redirectURL],
|
||||
response_types: ['code'],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates state, nonce and authorization url.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Setting} row
|
||||
* @return { {String}, {String}, {String} } state, nonce and url
|
||||
* */
|
||||
let getInitParams = async (req, row) => {
|
||||
let client = await getClient(row),
|
||||
state = crypto.randomUUID(),
|
||||
nonce = crypto.randomUUID(),
|
||||
url = client.authorizationUrl({
|
||||
scope: 'openid email profile',
|
||||
resource: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
||||
state,
|
||||
nonce,
|
||||
});
|
||||
|
||||
return { state, nonce, url };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses state and nonce from cookie during the callback phase.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @return { {String}, {String} } state and nonce
|
||||
* */
|
||||
let parseStateFromCookie = (req) => {
|
||||
let state, nonce;
|
||||
let cookies = req.headers.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
if (cookie.split('=')[0].trim() === 'npm_oidc') {
|
||||
let raw = cookie.split('=')[1],
|
||||
val = raw.split('--');
|
||||
state = val[0].trim();
|
||||
nonce = val[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { state, nonce };
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes validation of callback parameters.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Setting} settings
|
||||
* @return {Promise} a promise resolving to a jwt token
|
||||
* */
|
||||
let validateCallback = async (req, settings) => {
|
||||
let client = await getClient(settings);
|
||||
let { state, nonce } = parseStateFromCookie(req);
|
||||
|
||||
const params = client.callbackParams(req);
|
||||
const tokenSet = await client.callback(settings.meta.redirectURL, params, { state, nonce });
|
||||
let claims = tokenSet.claims();
|
||||
|
||||
if (!claims.email) {
|
||||
throw new error.AuthError('The Identity Provider didn\'t send the \'email\' claim');
|
||||
} else {
|
||||
logger.info('Successful authentication for email ' + claims.email);
|
||||
}
|
||||
|
||||
return internalToken.getTokenFromOAuthClaim({ identity: claims.email });
|
||||
};
|
||||
|
||||
let redirectToAuthorizationURL = (res, params) => {
|
||||
logger.info('Authorization URL: ' + params.url);
|
||||
res.cookie('npm_oidc', params.state + '--' + params.nonce);
|
||||
res.redirect(params.url);
|
||||
};
|
||||
|
||||
let redirectWithJwtToken = (res, token) => {
|
||||
res.cookie('npm_oidc', token.token + '---' + token.expires);
|
||||
res.redirect('/login');
|
||||
};
|
||||
|
||||
let redirectWithError = (res, error) => {
|
||||
logger.error('Callback error: ' + error.message);
|
||||
res.cookie('npm_oidc_error', error.message);
|
||||
res.redirect('/login');
|
||||
};
|
||||
|
||||
module.exports = router;
|
@ -69,17 +69,6 @@ router
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
if (row.id === 'oidc-config') {
|
||||
// redact oidc configuration via api
|
||||
let m = row.meta;
|
||||
row.meta = {
|
||||
name: m.name,
|
||||
enabled: m.enabled === true && !!(m.clientID && m.clientSecret && m.issuerURL && m.redirectURL && m.name)
|
||||
};
|
||||
// remove these temporary cookies used during oidc authentication
|
||||
res.clearCookie('npm_oidc');
|
||||
res.clearCookie('npm_oidc_error');
|
||||
}
|
||||
res.status(200)
|
||||
.send(row);
|
||||
})
|
||||
|
@ -28,8 +28,6 @@ router
|
||||
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
|
||||
})
|
||||
.then((data) => {
|
||||
// clear this temporary cookie following a successful oidc authentication
|
||||
res.clearCookie('npm_oidc');
|
||||
res.status(200)
|
||||
.send(data);
|
||||
})
|
||||
|
@ -235,6 +235,43 @@
|
||||
"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"
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,30 @@
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@ -161,6 +185,30 @@
|
||||
"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"
|
||||
},
|
||||
"openidc_restrict_users_enabled": {
|
||||
"$ref": "#/definitions/openidc_restrict_users_enabled"
|
||||
},
|
||||
"openidc_allowed_users": {
|
||||
"$ref": "#/definitions/openidc_allowed_users"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/definitions/enabled"
|
||||
},
|
||||
@ -251,6 +299,30 @@
|
||||
"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"
|
||||
},
|
||||
"openidc_restrict_users_enabled": {
|
||||
"$ref": "#/definitions/openidc_restrict_users_enabled"
|
||||
},
|
||||
"openidc_allowed_users": {
|
||||
"$ref": "#/definitions/openidc_allowed_users"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/definitions/enabled"
|
||||
},
|
||||
@ -324,6 +396,30 @@
|
||||
"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"
|
||||
},
|
||||
"openidc_restrict_users_enabled": {
|
||||
"$ref": "#/definitions/openidc_restrict_users_enabled"
|
||||
},
|
||||
"openidc_allowed_users": {
|
||||
"$ref": "#/definitions/openidc_allowed_users"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/definitions/enabled"
|
||||
},
|
||||
|
@ -75,8 +75,7 @@ const setupDefaultUser = () => {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const setupDefaultSettings = () => {
|
||||
return Promise.all([
|
||||
settingModel
|
||||
return settingModel
|
||||
.query()
|
||||
.select(settingModel.raw('COUNT(`id`) as `count`'))
|
||||
.where({id: 'default-site'})
|
||||
@ -93,38 +92,13 @@ const setupDefaultSettings = () => {
|
||||
meta: {},
|
||||
})
|
||||
.then(() => {
|
||||
logger.info('Added default-site setting');
|
||||
logger.info('Default settings added');
|
||||
});
|
||||
}
|
||||
if (config.debug()) {
|
||||
logger.info('Default setting setup not required');
|
||||
}
|
||||
}),
|
||||
settingModel
|
||||
.query()
|
||||
.select(settingModel.raw('COUNT(`id`) as `count`'))
|
||||
.where({id: 'oidc-config'})
|
||||
.first()
|
||||
.then((row) => {
|
||||
if (!row.count) {
|
||||
settingModel
|
||||
.query()
|
||||
.insert({
|
||||
id: 'oidc-config',
|
||||
name: 'Open ID Connect',
|
||||
description: 'Sign in to Nginx Proxy Manager with an external Identity Provider',
|
||||
value: 'metadata',
|
||||
meta: {},
|
||||
})
|
||||
.then(() => {
|
||||
logger.info('Added oidc-config setting');
|
||||
});
|
||||
}
|
||||
if (config.debug()) {
|
||||
logger.info('Default setting setup not required');
|
||||
}
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
47
backend/templates/_openid_connect.conf
Normal file
47
backend/templates/_openid_connect.conf
Normal file
@ -0,0 +1,47 @@
|
||||
{% if openidc_enabled == 1 or openidc_enabled == true -%}
|
||||
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
|
||||
|
||||
{% 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
|
||||
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)
|
||||
ngx.req.set_header("X-OIDC-NAME", res.id_token.name)
|
||||
}
|
||||
{% endif %}
|
@ -33,8 +33,30 @@ proxy_http_version 1.1;
|
||||
|
||||
location / {
|
||||
|
||||
{% include "_access.conf" %}
|
||||
{% include "_hsts.conf" %}
|
||||
{% if access_list_id > 0 %}
|
||||
{% if access_list.items.length > 0 %}
|
||||
# Authorization
|
||||
auth_basic "Authorization required";
|
||||
auth_basic_user_file /data/access/{{ access_list_id }};
|
||||
|
||||
{{ access_list.passauth }}
|
||||
{% endif %}
|
||||
|
||||
# Access Rules
|
||||
{% for client in access_list.clients %}
|
||||
{{- client.rule -}};
|
||||
{% endfor %}deny all;
|
||||
|
||||
# Access checks must...
|
||||
{% if access_list.satisfy %}
|
||||
{{ access_list.satisfy }};
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% include "_openid_connect.conf" %}
|
||||
{% include "_access.conf" %}
|
||||
{% include "_hsts.conf" %}
|
||||
|
||||
{% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
|
1903
backend/yarn.lock
1903
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -46,7 +46,21 @@ 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;
|
||||
|
||||
# Log format and fallback log file
|
||||
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"';
|
||||
|
||||
access_log /data/logs/fallback_access.log proxy;
|
||||
|
||||
include /etc/nginx/conf.d/include/log.conf;
|
||||
|
||||
# Dynamically generated resolvers file
|
||||
|
@ -200,6 +200,28 @@ value by specifying it as a Docker environment variable. The default if not spec
|
||||
...
|
||||
```
|
||||
|
||||
## 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://<proxy host url>/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://<provider URL>/.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.
|
||||
|
||||
## Customising logrotate settings
|
||||
|
||||
By default, NPM rotates the access- and error logs weekly and keeps 4 and 10 log files respectively.
|
||||
|
@ -59,8 +59,6 @@ function fetch(verb, path, data, options) {
|
||||
},
|
||||
|
||||
beforeSend: function (xhr) {
|
||||
// allow unauthenticated access to OIDC configuration
|
||||
if (path === 'settings/oidc-config') return;
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
|
||||
},
|
||||
|
||||
|
@ -434,11 +434,6 @@ module.exports = {
|
||||
App.UI.showModalDialog(new View({model: model}));
|
||||
});
|
||||
}
|
||||
if (model.get('id') === 'oidc-config') {
|
||||
require(['./main', './settings/oidc-config/main'], function (App, View) {
|
||||
App.UI.showModalDialog(new View({model: model}));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
<li role="presentation" class="nav-item"><a href="#locations" aria-controls="tab4" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-layers"></i> <%- i18n('all-hosts', 'locations') %></a></li>
|
||||
<li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
|
||||
<li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></a></li>
|
||||
<li role="presentation" class="nav-item"><a href="#openidc" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i><%- i18n('proxy-hosts', 'oidc') %></a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
|
||||
@ -271,6 +272,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenID Connect -->
|
||||
<div role="tabpanel" class="tab-pane" id="openidc">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="form-group">
|
||||
<label class="custom-switch">
|
||||
<input type="checkbox" class="custom-switch-input" name="openidc_enabled" value="1"<%- openidc_enabled ? ' checked' : '' %>>
|
||||
<span class="custom-switch-indicator"></span>
|
||||
<span class="custom-switch-description"><%- i18n('proxy-hosts', 'oidc-enabled') %></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 openidc">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('proxy-hosts', 'oidc-redirect-uri') %><span class="form-required">*</span></label>
|
||||
<input type="text" name="openidc_redirect_uri" class="form-control text-monospace" placeholder="" value="<%- openidc_redirect_uri %>" autocomplete="off" maxlength="255" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 openidc">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('proxy-hosts', 'oidc-discovery-endpoint') %><span class="form-required">*</span></label>
|
||||
<input type="text" name="openidc_discovery" class="form-control text-monospace" placeholder="" value="<%- openidc_discovery %>" autocomplete="off" maxlength="255" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 openidc">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('proxy-hosts', 'oidc-token-auth-method') %><span class="form-required">*</span></label>
|
||||
<select name="openidc_auth_method" class="form-control custom-select" placeholder="client_secret_post">
|
||||
<option value="client_secret_post" <%- openidc_auth_method === 'client_secret_post' ? 'selected' : '' %>>client_secret_post</option>
|
||||
<option value="client_secret_basic" <%- openidc_auth_method === 'client_secret_basic' ? 'selected' : '' %>>client_secret_basic</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 openidc">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('proxy-hosts', 'oidc-client-id') %><span class="form-required">*</span></label>
|
||||
<input type="text" name="openidc_client_id" class="form-control text-monospace" placeholder="" value="<%- openidc_client_id %>" autocomplete="off" maxlength="255" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 openidc">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('proxy-hosts', 'oidc-client-secret') %><span class="form-required">*</span></label>
|
||||
<input type="text" name="openidc_client_secret" class="form-control text-monospace" placeholder="" value="<%- openidc_client_secret %>" autocomplete="off" maxlength="255" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openidc">
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="form-group">
|
||||
<label class="custom-switch">
|
||||
<input type="checkbox" class="custom-switch-input" name="openidc_restrict_users_enabled" value="1"<%- openidc_restrict_users_enabled ? ' checked' : '' %>>
|
||||
<span class="custom-switch-indicator"></span>
|
||||
<span class="custom-switch-description"><%- i18n('proxy-hosts', 'oidc-allow-only-emails') %></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 openidc_users">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('proxy-hosts', 'oidc-allowed-emails') %><span class="form-required">*</span></label>
|
||||
<input type="text" name="openidc_allowed_users" class="form-control" id="openidc_allowed_users" value="<%- openidc_allowed_users.join(',') %>" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -43,7 +43,12 @@ 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_restrict_users_enabled: 'input[name="openidc_restrict_users_enabled"]',
|
||||
openidc_allowed_users: 'input[name="openidc_allowed_users"]',
|
||||
openidc: '.openidc',
|
||||
openidc_users: '.openidc_users',
|
||||
},
|
||||
|
||||
regions: {
|
||||
@ -129,6 +134,27 @@ module.exports = Mn.View.extend({
|
||||
}
|
||||
},
|
||||
|
||||
'change @ui.openidc_enabled': function () {
|
||||
let checked = this.ui.openidc_enabled.prop('checked');
|
||||
|
||||
if (checked) {
|
||||
this.ui.openidc.show().find('input').prop('disabled', false);
|
||||
} 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 () {
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
'click @ui.add_location_btn': function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -167,6 +193,14 @@ 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';
|
||||
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;
|
||||
@ -203,6 +237,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 +384,23 @@ module.exports = Mn.View.extend({
|
||||
view.ui.certificate_select[0].selectize.setValue(view.model.get('certificate_id'));
|
||||
}
|
||||
});
|
||||
|
||||
// OpenID Connect
|
||||
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) {
|
||||
|
@ -1,19 +1,7 @@
|
||||
<td>
|
||||
<div>
|
||||
<% if (id === 'default-site') { %>
|
||||
<%- i18n('settings', 'default-site') %>
|
||||
<% } %>
|
||||
<% if (id === 'oidc-config') { %>
|
||||
<%- i18n('settings', 'oidc-config') %>
|
||||
<% } %>
|
||||
</div>
|
||||
<div><%- i18n('settings', 'default-site') %></div>
|
||||
<div class="small text-muted">
|
||||
<% if (id === 'default-site') { %>
|
||||
<%- i18n('settings', 'default-site-description') %>
|
||||
<% } %>
|
||||
<% if (id === 'oidc-config') { %>
|
||||
<%- i18n('settings', 'oidc-config-description') %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@ -21,14 +9,6 @@
|
||||
<% if (id === 'default-site') { %>
|
||||
<%- i18n('settings', 'default-site-' + value) %>
|
||||
<% } %>
|
||||
<% if (id === 'oidc-config' && meta && meta.name && meta.clientID && meta.clientSecret && meta.issuerURL && meta.redirectURL) { %>
|
||||
<%- meta.name %>
|
||||
<% if (!meta.enabled) { %>
|
||||
(<%- i18n('str', 'disabled') %>)
|
||||
<% } %>
|
||||
<% } else if (id === 'oidc-config') { %>
|
||||
<%- i18n('settings', 'oidc-not-configured') %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
|
@ -1,56 +0,0 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><%- i18n('settings', id) %></h5>
|
||||
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="form-group">
|
||||
<div class="form-label"><%- description %></div>
|
||||
<div>
|
||||
<p><%- i18n('settings', 'oidc-config-hint-1') %></p>
|
||||
<p><%- i18n('settings', 'oidc-config-hint-2') %></p>
|
||||
</div>
|
||||
<div class="custom-controls-stacked">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('str', 'name') %> <span class="form-required">*</span>
|
||||
<input class="form-control name-input" name="meta[name]" required type="text" value="<%- meta && typeof meta.name !== 'undefined' ? meta.name : '' %>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Client ID <span class="form-required">*</span>
|
||||
<input class="form-control id-input" name="meta[clientID]" required type="text" value="<%- meta && typeof meta.clientID !== 'undefined' ? meta.clientID : '' %>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Client Secret <span class="form-required">*</span>
|
||||
<input class="form-control secret-input" name="meta[clientSecret]" required type="text" value="<%- meta && typeof meta.clientSecret !== 'undefined' ? meta.clientSecret : '' %>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Issuer URL <span class="form-required">*</span>
|
||||
<input class="form-control issuer-input" name="meta[issuerURL]" required placeholder="https://" type="url" value="<%- meta && typeof meta.issuerURL !== 'undefined' ? meta.issuerURL : '' %>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Redirect URL <span class="form-required">*</span>
|
||||
<input class="form-control redirect-url-input" name="meta[redirectURL]" required placeholder="https://" type="url" value="<%- meta && typeof meta.redirectURL !== 'undefined' ? meta.redirectURL : document.location.origin + '/api/oidc/callback' %>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-label"><%- i18n('str', 'enable') %></div>
|
||||
<input class="form-check enabled-input" name="meta[enabled]" placeholder="" type="checkbox" <%- meta && (typeof meta.enabled !== 'undefined' && meta.enabled === true) || (JSON.stringify(meta) === '{}') ? 'checked="checked"' : '' %> >
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
|
||||
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
|
||||
</div>
|
||||
</div>
|
@ -1,46 +0,0 @@
|
||||
const Mn = require('backbone.marionette');
|
||||
const App = require('../../main');
|
||||
const template = require('./main.ejs');
|
||||
|
||||
require('jquery-serializejson');
|
||||
|
||||
module.exports = Mn.View.extend({
|
||||
template: template,
|
||||
className: 'modal-dialog wide',
|
||||
|
||||
ui: {
|
||||
form: 'form',
|
||||
buttons: '.modal-footer button',
|
||||
cancel: 'button.cancel',
|
||||
save: 'button.save',
|
||||
},
|
||||
|
||||
events: {
|
||||
'click @ui.save': function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.ui.form[0].checkValidity()) {
|
||||
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
|
||||
return;
|
||||
}
|
||||
|
||||
let view = this;
|
||||
let data = this.ui.form.serializeJSON();
|
||||
data.id = this.model.get('id');
|
||||
if (data.meta.enabled) {
|
||||
data.meta.enabled = data.meta.enabled === 'on' || data.meta.enabled === 'true';
|
||||
}
|
||||
|
||||
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
|
||||
App.Api.Settings.update(data)
|
||||
.then((result) => {
|
||||
view.model.set(result);
|
||||
App.UI.closeModal();
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err.message);
|
||||
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -5,7 +5,6 @@
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"sign-in": "Sign in",
|
||||
"sign-in-with": "Sign in with",
|
||||
"sign-out": "Sign out",
|
||||
"try-again": "Try again",
|
||||
"name": "Name",
|
||||
@ -133,6 +132,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",
|
||||
"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",
|
||||
"custom-forward-host-help": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path/",
|
||||
"search": "Search Host…"
|
||||
},
|
||||
@ -291,12 +300,7 @@
|
||||
"default-site-404": "404 Page",
|
||||
"default-site-444": "No Response (444)",
|
||||
"default-site-html": "Custom Page",
|
||||
"default-site-redirect": "Redirect",
|
||||
"oidc-config": "Open ID Conncect Configuration",
|
||||
"oidc-config-description": "Sign in to Nginx Proxy Manager with an external Identity Provider",
|
||||
"oidc-not-configured": "Not configured",
|
||||
"oidc-config-hint-1": "Provide configuration for an IdP that supports Open ID Connect Discovery.",
|
||||
"oidc-config-hint-2": "The 'RedirectURL' must be set to '[base URL]/api/oidc/callback', the IdP must send the 'email' claim and a user with matching email address must exist in Nginx Proxy Manager."
|
||||
"default-site-redirect": "Redirect"
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<div class="card-body p-6">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-6 margin-auto">
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<div class="text-center p-6">
|
||||
<img src="/images/logo-text-vertical-grey.png" alt="Logo" />
|
||||
<div class="text-center text-muted mt-5">
|
||||
@ -27,13 +27,6 @@
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
|
||||
</div>
|
||||
<div class="form-footer login-oidc">
|
||||
<div class="separator"><slot>OR</slot></div>
|
||||
<button type="button" id="login-oidc" class="btn btn-teal btn-block">
|
||||
<%- i18n('str', 'sign-in-with') %> <span class="oidc-provider"></span>
|
||||
</button>
|
||||
<div class="invalid-feedback oidc-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,7 +3,6 @@ const Mn = require('backbone.marionette');
|
||||
const template = require('./login.ejs');
|
||||
const Api = require('../../app/api');
|
||||
const i18n = require('../../app/i18n');
|
||||
const Tokens = require('../../app/tokens');
|
||||
|
||||
module.exports = Mn.View.extend({
|
||||
template: template,
|
||||
@ -14,11 +13,7 @@ module.exports = Mn.View.extend({
|
||||
identity: 'input[name="identity"]',
|
||||
secret: 'input[name="secret"]',
|
||||
error: '.secret-error',
|
||||
button: 'button[type=submit]',
|
||||
oidcLogin: 'div.login-oidc',
|
||||
oidcButton: 'button#login-oidc',
|
||||
oidcError: '.oidc-error',
|
||||
oidcProvider: 'span.oidc-provider'
|
||||
button: 'button'
|
||||
},
|
||||
|
||||
events: {
|
||||
@ -31,56 +26,10 @@ module.exports = Mn.View.extend({
|
||||
.then(() => {
|
||||
window.location = '/';
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
this.ui.error.text(err.message).show();
|
||||
this.ui.button.removeClass('btn-loading').prop('disabled', false);
|
||||
});
|
||||
},
|
||||
'click @ui.oidcButton': function() {
|
||||
this.ui.identity.prop('disabled', true);
|
||||
this.ui.secret.prop('disabled', true);
|
||||
this.ui.button.prop('disabled', true);
|
||||
this.ui.oidcButton.addClass('btn-loading').prop('disabled', true);
|
||||
// redirect to initiate oauth flow
|
||||
document.location.replace('/api/oidc/');
|
||||
},
|
||||
},
|
||||
|
||||
async onRender() {
|
||||
// read oauth callback state cookies
|
||||
let cookies = document.cookie.split(';'),
|
||||
token, expiry, error;
|
||||
for (cookie of cookies) {
|
||||
let raw = cookie.split('='),
|
||||
name = raw[0].trim(),
|
||||
value = raw[1];
|
||||
if (name === 'npm_oidc') {
|
||||
let v = value.split('---');
|
||||
token = v[0];
|
||||
expiry = v[1];
|
||||
}
|
||||
if (name === 'npm_oidc_error') {
|
||||
error = decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
|
||||
// register a newly acquired jwt token following successful oidc authentication
|
||||
if (token && expiry && (new Date(Date.parse(decodeURIComponent(expiry)))) > new Date() ) {
|
||||
Tokens.addToken(token);
|
||||
document.location.replace('/');
|
||||
}
|
||||
|
||||
// show error message following a failed oidc authentication
|
||||
if (error) {
|
||||
this.ui.oidcError.html(error);
|
||||
}
|
||||
|
||||
// fetch oidc configuration and show alternative action button if enabled
|
||||
let response = await Api.Settings.getById('oidc-config');
|
||||
if (response && response.meta && response.meta.enabled === true) {
|
||||
this.ui.oidcProvider.html(response.meta.name);
|
||||
this.ui.oidcLogin.show();
|
||||
this.ui.oidcError.show();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -22,6 +22,14 @@ const model = Backbone.Model.extend({
|
||||
block_exploits: false,
|
||||
http2_support: false,
|
||||
advanced_config: '',
|
||||
openidc_enabled: false,
|
||||
openidc_redirect_uri: '',
|
||||
openidc_discovery: '',
|
||||
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:
|
||||
|
@ -40,33 +40,3 @@ a:hover {
|
||||
.col-login {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.margin-auto {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.separator::before, .separator::after {
|
||||
content: "";
|
||||
flex: 1 1 0%;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.separator:not(:empty)::before {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.separator:not(:empty)::after {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.login-oidc {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
}
|
Reference in New Issue
Block a user