From caeb2934f0dff0e6b7d73b9bbeddb74a2f31116d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=9CL=C3=96P?= Date: Fri, 24 Feb 2023 08:39:21 +0000 Subject: [PATCH 01/18] FEAT: Add Open ID Connect authentication method * add `oidc-config` setting allowing an admin user to configure parameters * modify login page to show another button when oidc is configured * add dependency `openid-client` `v5.4.0` * add backend route to process "OAuth2 Authorization Code" flow initialisation * add backend route to process callback of above flow * sign in the authenticated user with internal jwt token if internal user with email matching the one retrieved from oauth claims exists Note: Only Open ID Connect Discovery is supported which most modern Identity Providers offer. Tested with Authentik 2023.2.2 and Keycloak 18.0.2 --- backend/internal/token.js | 46 ++++++ backend/lib/express/jwt-decode.js | 4 +- backend/package.json | 1 + backend/routes/api/main.js | 1 + backend/routes/api/oidc.js | 132 ++++++++++++++++++ backend/routes/api/settings.js | 11 ++ backend/routes/api/tokens.js | 2 + backend/yarn.lock | 37 +++++ frontend/js/app/api.js | 2 + frontend/js/app/controller.js | 5 + frontend/js/app/settings/list/item.ejs | 8 ++ frontend/js/app/settings/oidc-config/main.ejs | 47 +++++++ frontend/js/app/settings/oidc-config/main.js | 47 +++++++ frontend/js/i18n/messages.json | 1 + frontend/js/login/ui/login.ejs | 9 +- frontend/js/login/ui/login.js | 65 ++++++++- frontend/scss/custom.scss | 30 ++++ 17 files changed, 441 insertions(+), 7 deletions(-) create mode 100644 backend/routes/api/oidc.js create mode 100644 frontend/js/app/settings/oidc-config/main.ejs create mode 100644 frontend/js/app/settings/oidc-config/main.js diff --git a/backend/internal/token.js b/backend/internal/token.js index a64b9010..8e04341d 100644 --- a/backend/internal/token.js +++ b/backend/internal/token.js @@ -82,6 +82,52 @@ module.exports = { }); }, + /** + * @param {Object} data + * @param {String} data.identity + * @param {String} [issuer] + * @returns {Promise} + */ + getTokenFromOAuthClaim: (data, issuer) => { + 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] diff --git a/backend/lib/express/jwt-decode.js b/backend/lib/express/jwt-decode.js index 17edccec..745763a7 100644 --- a/backend/lib/express/jwt-decode.js +++ b/backend/lib/express/jwt-decode.js @@ -4,7 +4,9 @@ module.exports = () => { return function (req, res, next) { res.locals.access = null; let access = new Access(res.locals.token || null); - access.load() + // allow unauthenticated access to OIDC configuration + let anon_access = req.url === '/oidc-config' && !access.token.getUserId(); + access.load(anon_access) .then(() => { res.locals.access = access; next(); diff --git a/backend/package.json b/backend/package.json index bc682106..f90c2640 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "node-rsa": "^1.0.8", "nodemon": "^2.0.2", "objection": "^2.2.16", + "openid-client": "^5.4.0", "path": "^0.12.7", "signale": "^1.4.0", "sqlite3": "^4.1.1", diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js index 33cbbc21..2f3ec6d7 100644 --- a/backend/routes/api/main.js +++ b/backend/routes/api/main.js @@ -27,6 +27,7 @@ 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')); diff --git a/backend/routes/api/oidc.js b/backend/routes/api/oidc.js new file mode 100644 index 00000000..e60949b3 --- /dev/null +++ b/backend/routes/api/oidc.js @@ -0,0 +1,132 @@ +const crypto = require('crypto'); +const express = require('express'); +const jwtdecode = require('../../lib/express/jwt-decode'); +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 +}); + +/** + * OAuth Authorization Code flow initialisation + * + * /api/oidc + */ +router + .route('/') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/users + * + * Retrieve all users + */ + .get(jwtdecode(), async (req, res, next) => { + console.log("oidc init >>>", res.locals.access, oidc); + + settingModel + .query() + .where({id: 'oidc-config'}) + .first() + .then( async row => { + console.log('oidc init > config > ', row); + + let issuer = await oidc.Issuer.discover(row.meta.issuerURL); + let client = new issuer.Client({ + client_id: row.meta.clientID, + client_secret: row.meta.clientSecret, + redirect_uris: [row.meta.redirectURL], + response_types: ['code'], + }) + let state = crypto.randomUUID(); + let nonce = crypto.randomUUID(); + let url = client.authorizationUrl({ + scope: 'openid email profile', + resource: 'http://rye.local:2081/api/oidc/callback', + state, + nonce, + }) + + console.log('oidc init > url > ', state, nonce, url); + + res.cookie("npm_oidc", state + '--' + nonce); + res.redirect(url); + }); + }); + + +/** + * Oauth Authorization Code flow callback + * + * /api/oidc/callback + */ +router + .route('/callback') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /users/123 or /users/me + * + * Retrieve a specific user + */ + .get(jwtdecode(), async (req, res, next) => { + console.log("oidc callback >>>"); + + settingModel + .query() + .where({id: 'oidc-config'}) + .first() + .then( async row => { + console.log('oidc callback > config > ', row); + + let issuer = await oidc.Issuer.discover(row.meta.issuerURL); + let client = new issuer.Client({ + client_id: row.meta.clientID, + client_secret: row.meta.clientSecret, + redirect_uris: [row.meta.redirectURL], + response_types: ['code'], + }); + + let state, nonce; + let cookies = req.headers.cookie.split(';'); + for (cookie of cookies) { + if (cookie.split('=')[0].trim() === 'npm_oidc') { + let raw = cookie.split('=')[1]; + let val = raw.split('--'); + state = val[0].trim(); + nonce = val[1].trim(); + break; + } + } + + const params = client.callbackParams(req); + const tokenSet = await client.callback(row.meta.redirectURL, params, { /*code_verifier: verifier,*/ state, nonce }); + let claims = tokenSet.claims(); + console.log('validated ID Token claims %j', claims); + + return internalToken.getTokenFromOAuthClaim({ identity: claims.email }) + + }) + .then( response => { + console.log('oidc callback > signed token > >', response); + res.cookie('npm_oidc', response.token + '---' + response.expires); + res.redirect('/login'); + }) + .catch( err => { + console.log('oidc callback ERR > ', err); + res.cookie('npm_oidc_error', err.message); + res.redirect('/login'); + }); + }); + +module.exports = router; diff --git a/backend/routes/api/settings.js b/backend/routes/api/settings.js index d08b2bf5..edb9edd8 100644 --- a/backend/routes/api/settings.js +++ b/backend/routes/api/settings.js @@ -69,6 +69,17 @@ 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); }) diff --git a/backend/routes/api/tokens.js b/backend/routes/api/tokens.js index a21f998a..29bfbbaf 100644 --- a/backend/routes/api/tokens.js +++ b/backend/routes/api/tokens.js @@ -28,6 +28,8 @@ 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); }) diff --git a/backend/yarn.lock b/backend/yarn.lock index 396e11c9..e7deee42 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1874,6 +1874,11 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +jose@^4.10.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.12.0.tgz#7f00cd2f82499b91623cd413b7b5287fd52651ed" + integrity sha512-wW1u3cK81b+SFcHjGC8zw87yuyUweEFe0UJirrXEw1NasW00eF7sZjeG3SLBGz001ozxQ46Y9sofDvhBmWFtXQ== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2142,6 +2147,13 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -2487,6 +2499,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -2527,6 +2544,11 @@ objection@^2.2.16: ajv "^6.12.6" db-errors "^0.2.3" +oidc-token-hash@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6" + integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -2553,6 +2575,16 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +openid-client@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.4.0.tgz#77f1cda14e2911446f16ea3f455fc7c405103eac" + integrity sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ== + dependencies: + jose "^4.10.0" + lru-cache "^6.0.0" + object-hash "^2.0.1" + oidc-token-hash "^5.0.1" + optionator@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -3719,6 +3751,11 @@ yallist@^3.0.0, yallist@^3.1.1: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index 6e33a6dc..b314b40b 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -59,6 +59,8 @@ 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)); }, diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js index ccb2978a..a2c112b3 100644 --- a/frontend/js/app/controller.js +++ b/frontend/js/app/controller.js @@ -434,6 +434,11 @@ 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})); + }); + } } }, diff --git a/frontend/js/app/settings/list/item.ejs b/frontend/js/app/settings/list/item.ejs index 4f81b450..21eae7ed 100644 --- a/frontend/js/app/settings/list/item.ejs +++ b/frontend/js/app/settings/list/item.ejs @@ -9,6 +9,14 @@ <% 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) { %> + (Disabled) + <% } %> + <% } else if (id === 'oidc-config') { %> + Not configured + <% } %> diff --git a/frontend/js/app/settings/oidc-config/main.ejs b/frontend/js/app/settings/oidc-config/main.ejs new file mode 100644 index 00000000..6bd7c342 --- /dev/null +++ b/frontend/js/app/settings/oidc-config/main.ejs @@ -0,0 +1,47 @@ + diff --git a/frontend/js/app/settings/oidc-config/main.js b/frontend/js/app/settings/oidc-config/main.js new file mode 100644 index 00000000..8fffcd69 --- /dev/null +++ b/frontend/js/app/settings/oidc-config/main.js @@ -0,0 +1,47 @@ +const Mn = require('backbone.marionette'); +const App = require('../../main'); +const template = require('./main.ejs'); + +require('jquery-serializejson'); +require('selectize'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + 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()) { + $('').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'); + }); + } + } +}); diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index aa544c7e..c6f90941 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -5,6 +5,7 @@ "username": "Username", "password": "Password", "sign-in": "Sign in", + "sign-in-with": "Sign in with", "sign-out": "Sign out", "try-again": "Try again", "name": "Name", diff --git a/frontend/js/login/ui/login.ejs b/frontend/js/login/ui/login.ejs index 693bc050..84aa90a0 100644 --- a/frontend/js/login/ui/login.ejs +++ b/frontend/js/login/ui/login.ejs @@ -5,7 +5,7 @@
-
+
Logo
@@ -27,6 +27,13 @@ +
diff --git a/frontend/js/login/ui/login.js b/frontend/js/login/ui/login.js index 757eb4e3..50064f24 100644 --- a/frontend/js/login/ui/login.js +++ b/frontend/js/login/ui/login.js @@ -3,17 +3,22 @@ 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, className: 'page-single', ui: { - form: 'form', - identity: 'input[name="identity"]', - secret: 'input[name="secret"]', - error: '.secret-error', - button: 'button' + form: 'form', + 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' }, events: { @@ -30,6 +35,56 @@ module.exports = Mn.View.extend({ this.ui.error.text(err.message).show(); this.ui.button.removeClass('btn-loading').prop('disabled', false); }); + }, + 'click @ui.oidcButton': function(e) { + 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') { + console.log(' ERROR 000 > ', value); + error = decodeURIComponent(value); + } + } + + console.log('login.js event > render', expiry, token); + // register a newly acquired jwt token following successful oidc authentication + if (token && expiry && (new Date(Date.parse(decodeURIComponent(expiry)))) > new Date() ) { + console.log('login.js event > render >>>'); + Tokens.addToken(token); + document.location.replace('/'); + } + + // show error message following a failed oidc authentication + if (error) { + console.log(' ERROR > ', 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(); } }, diff --git a/frontend/scss/custom.scss b/frontend/scss/custom.scss index 4037dcf6..30abfb8b 100644 --- a/frontend/scss/custom.scss +++ b/frontend/scss/custom.scss @@ -39,4 +39,34 @@ 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; } \ No newline at end of file From 3e2a411dfbd53ca4be6c61a96fb0d322eff92cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=9CL=C3=96P?= Date: Fri, 24 Feb 2023 15:05:57 +0000 Subject: [PATCH 02/18] chore: add oidc setting db entry during setup --- backend/setup.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/setup.js b/backend/setup.js index 47fd1e7b..fff6cfaa 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -150,6 +150,18 @@ const setupDefaultSettings = () => { .then(() => { logger.info('Default settings added'); }); + 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('Default settings added'); + }); } if (debug_mode) { logger.debug('Default setting setup not required'); From 457d1a75ba6124ec499a49433f3232f4ceeb1536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=9CL=C3=96P?= Date: Fri, 24 Feb 2023 15:11:08 +0000 Subject: [PATCH 03/18] chore: improve oidc setting ui --- frontend/js/app/settings/oidc-config/main.ejs | 29 ++++++++++++------- frontend/js/app/settings/oidc-config/main.js | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/frontend/js/app/settings/oidc-config/main.ejs b/frontend/js/app/settings/oidc-config/main.ejs index 6bd7c342..1395a920 100644 --- a/frontend/js/app/settings/oidc-config/main.ejs +++ b/frontend/js/app/settings/oidc-config/main.ejs @@ -9,26 +9,35 @@
<%- description %>
+
+

<%- i18n('settings', 'oidc-config-hint-1') %>

+

<%- i18n('settings', 'oidc-config-hint-2') %>

+
-
Name
- +
-
Client ID
- +
-
Client Secret
- +
-
Issuer URL
- +
-
Redirect URL
- +
Enabled
diff --git a/frontend/js/app/settings/oidc-config/main.js b/frontend/js/app/settings/oidc-config/main.js index 8fffcd69..b4499785 100644 --- a/frontend/js/app/settings/oidc-config/main.js +++ b/frontend/js/app/settings/oidc-config/main.js @@ -7,7 +7,7 @@ require('selectize'); module.exports = Mn.View.extend({ template: template, - className: 'modal-dialog', + className: 'modal-dialog wide', ui: { form: 'form', From 8350271e6f83dd7c581d5d935b058df7be9828fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=9CL=C3=96P?= Date: Fri, 24 Feb 2023 15:12:03 +0000 Subject: [PATCH 04/18] chore: add message texts --- frontend/js/i18n/messages.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index c6f90941..29c4d644 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -289,7 +289,10 @@ "default-site-congratulations": "Congratulations Page", "default-site-404": "404 Page", "default-site-html": "Custom Page", - "default-site-redirect": "Redirect" + "default-site-redirect": "Redirect", + "oidc-config": "Open ID Conncect Configuration", + "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." } } } From bc0b466a8e79911022ce38937e0dc832154d5b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Fri, 24 Feb 2023 16:30:45 +0000 Subject: [PATCH 05/18] refactor: improve code structure --- backend/routes/api/oidc.js | 171 ++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 69 deletions(-) diff --git a/backend/routes/api/oidc.js b/backend/routes/api/oidc.js index e60949b3..e3da2caa 100644 --- a/backend/routes/api/oidc.js +++ b/backend/routes/api/oidc.js @@ -29,36 +29,13 @@ router * Retrieve all users */ .get(jwtdecode(), async (req, res, next) => { - console.log("oidc init >>>", res.locals.access, oidc); - + console.log("oidc: init flow"); settingModel .query() .where({id: 'oidc-config'}) .first() - .then( async row => { - console.log('oidc init > config > ', row); - - let issuer = await oidc.Issuer.discover(row.meta.issuerURL); - let client = new issuer.Client({ - client_id: row.meta.clientID, - client_secret: row.meta.clientSecret, - redirect_uris: [row.meta.redirectURL], - response_types: ['code'], - }) - let state = crypto.randomUUID(); - let nonce = crypto.randomUUID(); - let url = client.authorizationUrl({ - scope: 'openid email profile', - resource: 'http://rye.local:2081/api/oidc/callback', - state, - nonce, - }) - - console.log('oidc init > url > ', state, nonce, url); - - res.cookie("npm_oidc", state + '--' + nonce); - res.redirect(url); - }); + .then( row => getInitParams(req, row)) + .then( params => redirectToAuthorizationURL(res, params)); }); @@ -80,53 +57,109 @@ router * Retrieve a specific user */ .get(jwtdecode(), async (req, res, next) => { - console.log("oidc callback >>>"); - + console.log("oidc: callback"); settingModel .query() .where({id: 'oidc-config'}) .first() - .then( async row => { - console.log('oidc callback > config > ', row); - - let issuer = await oidc.Issuer.discover(row.meta.issuerURL); - let client = new issuer.Client({ - client_id: row.meta.clientID, - client_secret: row.meta.clientSecret, - redirect_uris: [row.meta.redirectURL], - response_types: ['code'], - }); - - let state, nonce; - let cookies = req.headers.cookie.split(';'); - for (cookie of cookies) { - if (cookie.split('=')[0].trim() === 'npm_oidc') { - let raw = cookie.split('=')[1]; - let val = raw.split('--'); - state = val[0].trim(); - nonce = val[1].trim(); - break; - } - } - - const params = client.callbackParams(req); - const tokenSet = await client.callback(row.meta.redirectURL, params, { /*code_verifier: verifier,*/ state, nonce }); - let claims = tokenSet.claims(); - console.log('validated ID Token claims %j', claims); - - return internalToken.getTokenFromOAuthClaim({ identity: claims.email }) - - }) - .then( response => { - console.log('oidc callback > signed token > >', response); - res.cookie('npm_oidc', response.token + '---' + response.expires); - res.redirect('/login'); - }) - .catch( err => { - console.log('oidc callback ERR > ', err); - res.cookie('npm_oidc_error', err.message); - res.redirect('/login'); - }); + .then( settings => validateCallback(req, settings)) + .then( token => redirectWithJwtToken(res, token)) + .catch( err => redirectWithError(res, err)); }); +/** + * Executed discovery and returns the configured `openid-client` client + * + * @param {Setting} row + * */ +let getClient = async row => { + let issuer = await oidc.Issuer.discover(row.meta.issuerURL); + + 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); + let state = crypto.randomUUID(); + let nonce = crypto.randomUUID(); + let 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 (cookie of cookies) { + if (cookie.split('=')[0].trim() === 'npm_oidc') { + let raw = cookie.split('=')[1]; + let 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(); + console.log('oidc: authentication successful for email', claims.email); + + return internalToken.getTokenFromOAuthClaim({ identity: claims.email }) +} + +let redirectToAuthorizationURL = (res, params) => { + console.log('oidc: init flow > 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) => { + console.log('oidc: callback error: ', error); + res.cookie('npm_oidc_error', error.message); + res.redirect('/login'); +} + module.exports = router; From baee4641db474ce15ba9d5a64d9265193ea708c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Fri, 24 Feb 2023 18:54:38 +0000 Subject: [PATCH 06/18] chore: improve error handling --- backend/routes/api/oidc.js | 11 +++++++++-- frontend/js/app/settings/oidc-config/main.ejs | 2 +- frontend/js/app/settings/oidc-config/main.js | 1 - 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/routes/api/oidc.js b/backend/routes/api/oidc.js index e3da2caa..b02c503f 100644 --- a/backend/routes/api/oidc.js +++ b/backend/routes/api/oidc.js @@ -1,4 +1,5 @@ const crypto = require('crypto'); +const error = require('../../lib/error'); const express = require('express'); const jwtdecode = require('../../lib/express/jwt-decode'); const oidc = require('openid-client'); @@ -35,7 +36,8 @@ router .where({id: 'oidc-config'}) .first() .then( row => getInitParams(req, row)) - .then( params => redirectToAuthorizationURL(res, params)); + .then( params => redirectToAuthorizationURL(res, params)) + .catch( err => redirectWithError(res, err)); }); @@ -73,7 +75,12 @@ router * @param {Setting} row * */ let getClient = async row => { - let issuer = await oidc.Issuer.discover(row.meta.issuerURL); + 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, diff --git a/frontend/js/app/settings/oidc-config/main.ejs b/frontend/js/app/settings/oidc-config/main.ejs index 1395a920..15eb3981 100644 --- a/frontend/js/app/settings/oidc-config/main.ejs +++ b/frontend/js/app/settings/oidc-config/main.ejs @@ -41,7 +41,7 @@
Enabled
- > + >
diff --git a/frontend/js/app/settings/oidc-config/main.js b/frontend/js/app/settings/oidc-config/main.js index b4499785..34b16b57 100644 --- a/frontend/js/app/settings/oidc-config/main.js +++ b/frontend/js/app/settings/oidc-config/main.js @@ -3,7 +3,6 @@ const App = require('../../main'); const template = require('./main.ejs'); require('jquery-serializejson'); -require('selectize'); module.exports = Mn.View.extend({ template: template, From 6f98fa61e4991ed7b8bdbf177bea87c97c9238e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Fri, 24 Feb 2023 21:09:21 +0000 Subject: [PATCH 07/18] refactor: satisfy linter requirements --- backend/internal/token.js | 45 +++++----- backend/routes/api/main.js | 2 +- backend/routes/api/oidc.js | 88 ++++++++++---------- backend/routes/api/settings.js | 8 +- frontend/js/app/api.js | 2 +- frontend/js/app/settings/oidc-config/main.js | 14 ++-- frontend/js/login/ui/login.js | 14 ++-- 7 files changed, 84 insertions(+), 89 deletions(-) diff --git a/backend/internal/token.js b/backend/internal/token.js index 8e04341d..27da42b4 100644 --- a/backend/internal/token.js +++ b/backend/internal/token.js @@ -88,7 +88,7 @@ module.exports = { * @param {String} [issuer] * @returns {Promise} */ - getTokenFromOAuthClaim: (data, issuer) => { + getTokenFromOAuthClaim: (data) => { let Token = new TokenModel(); data.scope = 'user'; @@ -101,31 +101,26 @@ module.exports = { .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() - }; - }); - + 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() }; + }); + }); }, /** diff --git a/backend/routes/api/main.js b/backend/routes/api/main.js index 2f3ec6d7..546cc727 100644 --- a/backend/routes/api/main.js +++ b/backend/routes/api/main.js @@ -27,7 +27,7 @@ router.get('/', (req, res/*, next*/) => { router.use('/schema', require('./schema')); router.use('/tokens', require('./tokens')); -router.use('/oidc', require('./oidc')) +router.use('/oidc', require('./oidc')); router.use('/users', require('./users')); router.use('/audit-log', require('./audit-log')); router.use('/reports', require('./reports')); diff --git a/backend/routes/api/oidc.js b/backend/routes/api/oidc.js index b02c503f..6fd87c70 100644 --- a/backend/routes/api/oidc.js +++ b/backend/routes/api/oidc.js @@ -3,7 +3,7 @@ const error = require('../../lib/error'); const express = require('express'); const jwtdecode = require('../../lib/express/jwt-decode'); const oidc = require('openid-client'); -const settingModel = require('../../models/setting'); +const settingModel = require('../../models/setting'); const internalToken = require('../../internal/token'); let router = express.Router({ @@ -29,15 +29,15 @@ router * * Retrieve all users */ - .get(jwtdecode(), async (req, res, next) => { - console.log("oidc: init flow"); + .get(jwtdecode(), async (req, res) => { + console.log('oidc: init flow'); settingModel .query() .where({id: 'oidc-config'}) .first() - .then( row => getInitParams(req, row)) - .then( params => redirectToAuthorizationURL(res, params)) - .catch( err => redirectWithError(res, err)); + .then((row) => getInitParams(req, row)) + .then((params) => redirectToAuthorizationURL(res, params)) + .catch((err) => redirectWithError(res, err)); }); @@ -58,15 +58,15 @@ router * * Retrieve a specific user */ - .get(jwtdecode(), async (req, res, next) => { - console.log("oidc: callback"); + .get(jwtdecode(), async (req, res) => { + console.log('oidc: callback'); settingModel .query() .where({id: 'oidc-config'}) .first() - .then( settings => validateCallback(req, settings)) - .then( token => redirectWithJwtToken(res, token)) - .catch( err => redirectWithError(res, err)); + .then((settings) => validateCallback(req, settings)) + .then((token) => redirectWithJwtToken(res, token)) + .catch((err) => redirectWithError(res, err)); }); /** @@ -74,21 +74,21 @@ router * * @param {Setting} row * */ -let getClient = async row => { +let getClient = async (row) => { let issuer; try { issuer = await oidc.Issuer.discover(row.meta.issuerURL); - } catch(err) { + } 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], + client_id: row.meta.clientID, + client_secret: row.meta.clientSecret, + redirect_uris: [row.meta.redirectURL], response_types: ['code'], }); -} +}; /** * Generates state, nonce and authorization url. @@ -98,18 +98,18 @@ let getClient = async row => { * @return { {String}, {String}, {String} } state, nonce and url * */ let getInitParams = async (req, row) => { - let client = await getClient(row); - let state = crypto.randomUUID(); - let nonce = crypto.randomUUID(); - let url = client.authorizationUrl({ - scope: 'openid email profile', - resource: `${req.protocol}://${req.get('host')}${req.originalUrl}`, - state, - nonce, - }) + 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. @@ -117,21 +117,21 @@ let getInitParams = async (req, row) => { * @param {Request} req * @return { {String}, {String} } state and nonce * */ -let parseStateFromCookie = req => { +let parseStateFromCookie = (req) => { let state, nonce; let cookies = req.headers.cookie.split(';'); - for (cookie of cookies) { + for (let cookie of cookies) { if (cookie.split('=')[0].trim() === 'npm_oidc') { - let raw = cookie.split('=')[1]; - let val = raw.split('--'); - state = val[0].trim(); - nonce = val[1].trim(); + 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. @@ -140,33 +140,33 @@ let parseStateFromCookie = req => { * @param {Setting} settings * @return {Promise} a promise resolving to a jwt token * */ -let validateCallback = async (req, settings) => { - let client = await getClient(settings); +let validateCallback = async (req, settings) => { + let client = await getClient(settings); let { state, nonce } = parseStateFromCookie(req); - const params = client.callbackParams(req); + const params = client.callbackParams(req); const tokenSet = await client.callback(settings.meta.redirectURL, params, { state, nonce }); - let claims = tokenSet.claims(); + let claims = tokenSet.claims(); console.log('oidc: authentication successful for email', claims.email); - return internalToken.getTokenFromOAuthClaim({ identity: claims.email }) -} + return internalToken.getTokenFromOAuthClaim({ identity: claims.email }); +}; let redirectToAuthorizationURL = (res, params) => { console.log('oidc: init flow > url > ', params.url); - res.cookie("npm_oidc", params.state + '--' + params.nonce); + 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) => { console.log('oidc: callback error: ', error); res.cookie('npm_oidc_error', error.message); res.redirect('/login'); -} +}; module.exports = router; diff --git a/backend/routes/api/settings.js b/backend/routes/api/settings.js index edb9edd8..f04f3d7f 100644 --- a/backend/routes/api/settings.js +++ b/backend/routes/api/settings.js @@ -71,14 +71,14 @@ router .then((row) => { if (row.id === 'oidc-config') { // redact oidc configuration via api - let m = row.meta + let m = row.meta; row.meta = { - name: m.name, + 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.clearCookie('npm_oidc'); + res.clearCookie('npm_oidc_error'); } res.status(200) .send(row); diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index b314b40b..207cb548 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -60,7 +60,7 @@ function fetch(verb, path, data, options) { beforeSend: function (xhr) { // allow unauthenticated access to OIDC configuration - if (path === "settings/oidc-config") return; + if (path === 'settings/oidc-config') return; xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); }, diff --git a/frontend/js/app/settings/oidc-config/main.js b/frontend/js/app/settings/oidc-config/main.js index 34b16b57..b4eb6d1c 100644 --- a/frontend/js/app/settings/oidc-config/main.js +++ b/frontend/js/app/settings/oidc-config/main.js @@ -9,10 +9,10 @@ module.exports = Mn.View.extend({ className: 'modal-dialog wide', ui: { - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save', + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save', }, events: { @@ -28,16 +28,16 @@ module.exports = Mn.View.extend({ 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"; + 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 => { + .then((result) => { view.model.set(result); App.UI.closeModal(); }) - .catch(err => { + .catch((err) => { alert(err.message); this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); }); diff --git a/frontend/js/login/ui/login.js b/frontend/js/login/ui/login.js index 50064f24..dc5605d8 100644 --- a/frontend/js/login/ui/login.js +++ b/frontend/js/login/ui/login.js @@ -31,12 +31,12 @@ 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(e) { + 'click @ui.oidcButton': function() { this.ui.identity.prop('disabled', true); this.ui.secret.prop('disabled', true); this.ui.button.prop('disabled', true); @@ -51,12 +51,12 @@ module.exports = Mn.View.extend({ let cookies = document.cookie.split(';'), token, expiry, error; for (cookie of cookies) { - let raw = cookie.split('='), - name = raw[0].trim(), + let raw = cookie.split('='), + name = raw[0].trim(), value = raw[1]; if (name === 'npm_oidc') { - let v = value.split('---'); - token = v[0]; + let v = value.split('---'); + token = v[0]; expiry = v[1]; } if (name === 'npm_oidc_error') { @@ -80,7 +80,7 @@ module.exports = Mn.View.extend({ } // fetch oidc configuration and show alternative action button if enabled - let response = await Api.Settings.getById("oidc-config"); + 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(); From df5ab361e30fae36cba8b4dd2b419a0911746b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Fri, 24 Feb 2023 22:27:27 +0000 Subject: [PATCH 08/18] chore: update comments, remove debug logging --- backend/routes/api/oidc.js | 20 +++++--------------- frontend/js/login/ui/login.js | 4 ---- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/backend/routes/api/oidc.js b/backend/routes/api/oidc.js index 6fd87c70..58d2c062 100644 --- a/backend/routes/api/oidc.js +++ b/backend/routes/api/oidc.js @@ -12,11 +12,6 @@ let router = express.Router({ mergeParams: true }); -/** - * OAuth Authorization Code flow initialisation - * - * /api/oidc - */ router .route('/') .options((req, res) => { @@ -25,9 +20,9 @@ router .all(jwtdecode()) /** - * GET /api/users + * GET /api/oidc * - * Retrieve all users + * OAuth Authorization Code flow initialisation */ .get(jwtdecode(), async (req, res) => { console.log('oidc: init flow'); @@ -41,11 +36,6 @@ router }); -/** - * Oauth Authorization Code flow callback - * - * /api/oidc/callback - */ router .route('/callback') .options((req, res) => { @@ -54,9 +44,9 @@ router .all(jwtdecode()) /** - * GET /users/123 or /users/me + * GET /api/oidc/callback * - * Retrieve a specific user + * Oauth Authorization Code flow callback */ .get(jwtdecode(), async (req, res) => { console.log('oidc: callback'); @@ -70,7 +60,7 @@ router }); /** - * Executed discovery and returns the configured `openid-client` client + * Executes discovery and returns the configured `openid-client` client * * @param {Setting} row * */ diff --git a/frontend/js/login/ui/login.js b/frontend/js/login/ui/login.js index dc5605d8..0c1c25c6 100644 --- a/frontend/js/login/ui/login.js +++ b/frontend/js/login/ui/login.js @@ -60,22 +60,18 @@ module.exports = Mn.View.extend({ expiry = v[1]; } if (name === 'npm_oidc_error') { - console.log(' ERROR 000 > ', value); error = decodeURIComponent(value); } } - console.log('login.js event > render', expiry, token); // register a newly acquired jwt token following successful oidc authentication if (token && expiry && (new Date(Date.parse(decodeURIComponent(expiry)))) > new Date() ) { - console.log('login.js event > render >>>'); Tokens.addToken(token); document.location.replace('/'); } // show error message following a failed oidc authentication if (error) { - console.log(' ERROR > ', error); this.ui.oidcError.html(error); } From ef64edd9432b05f39063f790188df495dca04c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Sun, 26 Feb 2023 13:24:47 +0000 Subject: [PATCH 09/18] fix: add database migration for oidc-config setting --- .../20230226135501_add_oidc_config_segging.js | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 backend/migrations/20230226135501_add_oidc_config_segging.js diff --git a/backend/migrations/20230226135501_add_oidc_config_segging.js b/backend/migrations/20230226135501_add_oidc_config_segging.js new file mode 100644 index 00000000..bb37f7e6 --- /dev/null +++ b/backend/migrations/20230226135501_add_oidc_config_segging.js @@ -0,0 +1,42 @@ +const migrate_name = 'oidc_config_setting'; +const logger = require('../logger').migrate; +const settingModel = require('../models/setting'); + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.up = function (knex) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return 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: {}, + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @param {Promise} Promise + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return settingModel + .query() + .delete() + .where('setting_id', 'oidc-config'); +}; \ No newline at end of file From fd49644f212399cb2f6c27e75fcd176fe1baa89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Sun, 26 Feb 2023 13:34:58 +0000 Subject: [PATCH 10/18] fix: linter --- .../20230226135501_add_oidc_config_segging.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/migrations/20230226135501_add_oidc_config_segging.js b/backend/migrations/20230226135501_add_oidc_config_segging.js index bb37f7e6..7af85c7b 100644 --- a/backend/migrations/20230226135501_add_oidc_config_segging.js +++ b/backend/migrations/20230226135501_add_oidc_config_segging.js @@ -1,17 +1,16 @@ -const migrate_name = 'oidc_config_setting'; -const logger = require('../logger').migrate; -const settingModel = require('../models/setting'); +const migrate_name = 'oidc_config_setting'; +const settingModel = require('../models/setting'); +const logger = require('../logger').migrate; /** * Migrate * * @see http://knexjs.org/#Schema * - * @param {Object} knex * @param {Promise} Promise * @returns {Promise} */ -exports.up = function (knex) { +exports.up = function () { logger.info('[' + migrate_name + '] Migrating Up...'); return settingModel @@ -28,11 +27,10 @@ exports.up = function (knex) { /** * Undo Migrate * - * @param {Object} knex * @param {Promise} Promise * @returns {Promise} */ -exports.down = function (knex) { +exports.down = function () { logger.info('[' + migrate_name + '] Migrating Down...'); return settingModel From d0d36a95ec96396b81aa428c6332d3b6546c1cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Mon, 6 Mar 2023 09:33:01 +0000 Subject: [PATCH 11/18] fix: add oidc-config setting via setup.js rather than migrations --- .../20230226135501_add_oidc_config_segging.js | 40 ------------------ backend/setup.js | 42 ++++++++++++------- 2 files changed, 27 insertions(+), 55 deletions(-) delete mode 100644 backend/migrations/20230226135501_add_oidc_config_segging.js diff --git a/backend/migrations/20230226135501_add_oidc_config_segging.js b/backend/migrations/20230226135501_add_oidc_config_segging.js deleted file mode 100644 index 7af85c7b..00000000 --- a/backend/migrations/20230226135501_add_oidc_config_segging.js +++ /dev/null @@ -1,40 +0,0 @@ -const migrate_name = 'oidc_config_setting'; -const settingModel = require('../models/setting'); -const logger = require('../logger').migrate; - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function () { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return 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: {}, - }); -}; - -/** - * Undo Migrate - * - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function () { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return settingModel - .query() - .delete() - .where('setting_id', 'oidc-config'); -}; \ No newline at end of file diff --git a/backend/setup.js b/backend/setup.js index fff6cfaa..f36927c9 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -131,7 +131,7 @@ const setupDefaultUser = () => { * @returns {Promise} */ const setupDefaultSettings = () => { - return settingModel + return Promise.all([settingModel .query() .select(settingModel.raw('COUNT(`id`) as `count`')) .where({id: 'default-site'}) @@ -148,25 +148,37 @@ const setupDefaultSettings = () => { meta: {}, }) .then(() => { - logger.info('Default settings added'); - }); - 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('Default settings added'); + logger.info('Added default-site setting'); }); } if (debug_mode) { logger.debug('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 (debug_mode) { + logger.debug('Default setting setup not required'); + } + })]); }; /** From 6ed64153e76e7e0a562f73e103239d6bd7431e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Mon, 6 Mar 2023 12:27:51 +0000 Subject: [PATCH 12/18] fix: add oidc logger and replace console logging --- backend/logger.js | 3 ++- backend/routes/api/oidc.js | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/logger.js b/backend/logger.js index 680af6d5..3ece76fd 100644 --- a/backend/logger.js +++ b/backend/logger.js @@ -9,5 +9,6 @@ module.exports = { ssl: new Signale({scope: 'SSL '}), import: new Signale({scope: 'Importer '}), setup: new Signale({scope: 'Setup '}), - ip_ranges: new Signale({scope: 'IP Ranges'}) + ip_ranges: new Signale({scope: 'IP Ranges'}), + oidc: new Signale({scope: 'OIDC '}) }; diff --git a/backend/routes/api/oidc.js b/backend/routes/api/oidc.js index 58d2c062..9c8030f9 100644 --- a/backend/routes/api/oidc.js +++ b/backend/routes/api/oidc.js @@ -2,6 +2,7 @@ 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'); @@ -25,7 +26,7 @@ router * OAuth Authorization Code flow initialisation */ .get(jwtdecode(), async (req, res) => { - console.log('oidc: init flow'); + logger.info('Initializing OAuth flow'); settingModel .query() .where({id: 'oidc-config'}) @@ -49,7 +50,7 @@ router * Oauth Authorization Code flow callback */ .get(jwtdecode(), async (req, res) => { - console.log('oidc: callback'); + logger.info('Processing callback'); settingModel .query() .where({id: 'oidc-config'}) @@ -137,13 +138,18 @@ let validateCallback = async (req, settings) => { const params = client.callbackParams(req); const tokenSet = await client.callback(settings.meta.redirectURL, params, { state, nonce }); let claims = tokenSet.claims(); - console.log('oidc: authentication successful for email', claims.email); + + 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) => { - console.log('oidc: init flow > url > ', params.url); + logger.info('Authorization URL: ' + params.url); res.cookie('npm_oidc', params.state + '--' + params.nonce); res.redirect(params.url); }; @@ -154,7 +160,7 @@ let redirectWithJwtToken = (res, token) => { }; let redirectWithError = (res, error) => { - console.log('oidc: callback error: ', error); + logger.error('Callback error: ' + error.message); res.cookie('npm_oidc_error', error.message); res.redirect('/login'); }; From 0f588baa3e8a8d56d1ce0c3e167dd9168979952b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcell=20F=C3=BCl=C3=B6p?= Date: Thu, 9 Mar 2023 21:24:12 +0000 Subject: [PATCH 13/18] fix: indentation --- backend/setup.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/backend/setup.js b/backend/setup.js index f36927c9..68483525 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -155,30 +155,30 @@ const setupDefaultSettings = () => { logger.debug('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 (debug_mode) { - logger.debug('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 (debug_mode) { + logger.debug('Default setting setup not required'); + } + })]); }; /** From 8b841176fad5d99e3ce178224f058dba0150f6cc Mon Sep 17 00:00:00 2001 From: Samuel Oechsler Date: Thu, 19 Sep 2024 19:39:17 +0200 Subject: [PATCH 14/18] Fix configuration template --- backend/setup.js | 98 ++++++++++--------- frontend/js/app/settings/list/item.ejs | 20 +++- frontend/js/app/settings/oidc-config/main.ejs | 4 +- frontend/js/i18n/messages.json | 4 +- 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/backend/setup.js b/backend/setup.js index 26dd3f27..ec6b44fb 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -75,54 +75,56 @@ const setupDefaultUser = () => { * @returns {Promise} */ const setupDefaultSettings = () => { - return Promise.all([settingModel - .query() - .select(settingModel.raw('COUNT(`id`) as `count`')) - .where({id: 'default-site'}) - .first() - .then((row) => { - if (!row.count) { - settingModel - .query() - .insert({ - id: 'default-site', - name: 'Default Site', - description: 'What to show when Nginx is hit with an unknown Host', - value: 'congratulations', - meta: {}, - }) - .then(() => { - logger.info('Added default-site setting'); - }); - } - 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 (debug_mode) { - logger.debug('Default setting setup not required'); - } - })]); + return Promise.all([ + settingModel + .query() + .select(settingModel.raw('COUNT(`id`) as `count`')) + .where({id: 'default-site'}) + .first() + .then((row) => { + if (!row.count) { + settingModel + .query() + .insert({ + id: 'default-site', + name: 'Default Site', + description: 'What to show when Nginx is hit with an unknown Host', + value: 'congratulations', + meta: {}, + }) + .then(() => { + logger.info('Added default-site setting'); + }); + } + 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'); + } + }) + ]); }; /** diff --git a/frontend/js/app/settings/list/item.ejs b/frontend/js/app/settings/list/item.ejs index 4f32cd47..9afae591 100644 --- a/frontend/js/app/settings/list/item.ejs +++ b/frontend/js/app/settings/list/item.ejs @@ -1,7 +1,19 @@ -
<%- i18n('settings', 'default-site') %>
+
+ <% if (id === 'default-site') { %> + <%- i18n('settings', 'default-site') %> + <% } %> + <% if (id === 'oidc-config') { %> + <%- i18n('settings', 'oidc-config') %> + <% } %> +
- <%- i18n('settings', 'default-site-description') %> + <% if (id === 'default-site') { %> + <%- i18n('settings', 'default-site-description') %> + <% } %> + <% if (id === 'oidc-config') { %> + <%- i18n('settings', 'oidc-config-description') %> + <% } %>
@@ -12,10 +24,10 @@ <% if (id === 'oidc-config' && meta && meta.name && meta.clientID && meta.clientSecret && meta.issuerURL && meta.redirectURL) { %> <%- meta.name %> <% if (!meta.enabled) { %> - (Disabled) + (<%- i18n('str', 'disabled') %>) <% } %> <% } else if (id === 'oidc-config') { %> - Not configured + <%- i18n('settings', 'oidc-not-configured') %> <% } %>
diff --git a/frontend/js/app/settings/oidc-config/main.ejs b/frontend/js/app/settings/oidc-config/main.ejs index 15eb3981..d8d767a0 100644 --- a/frontend/js/app/settings/oidc-config/main.ejs +++ b/frontend/js/app/settings/oidc-config/main.ejs @@ -15,7 +15,7 @@
-
@@ -40,7 +40,7 @@
-
Enabled
+
<%- i18n('str', 'enable') %>
>
diff --git a/frontend/js/i18n/messages.json b/frontend/js/i18n/messages.json index e7dca961..3a3ec055 100644 --- a/frontend/js/i18n/messages.json +++ b/frontend/js/i18n/messages.json @@ -293,8 +293,10 @@ "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." } } -} +} \ No newline at end of file From 0b126ca5466fb30875bb4a59dc80a912667a937f Mon Sep 17 00:00:00 2001 From: Samuel Oechsler Date: Wed, 30 Oct 2024 20:33:26 +0100 Subject: [PATCH 15/18] Add oidc-config to OpenAPI schema --- .../schema/paths/settings/settingID/put.json | 73 +++++++++++++++---- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/backend/schema/paths/settings/settingID/put.json b/backend/schema/paths/settings/settingID/put.json index 4ca62429..d205158f 100644 --- a/backend/schema/paths/settings/settingID/put.json +++ b/backend/schema/paths/settings/settingID/put.json @@ -14,7 +14,7 @@ "schema": { "type": "string", "minLength": 1, - "enum": ["default-site"] + "enum": ["default-site", "oidc-config"] }, "required": true, "description": "Setting ID", @@ -27,28 +27,69 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "value": { - "type": "string", - "minLength": 1, - "enum": ["congratulations", "404", "444", "redirect", "html"] - }, - "meta": { + "oneOf": [ + { "type": "object", "additionalProperties": false, + "minProperties": 1, "properties": { - "redirect": { - "type": "string" + "value": { + "type": "string", + "minLength": 1, + "enum": [ + "congratulations", + "404", + "444", + "redirect", + "html" + ] }, - "html": { - "type": "string" + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "redirect": { + "type": "string" + }, + "html": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "clientID": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "issuerURL": { + "type": "string" + }, + "name": { + "type": "string" + }, + "redirectURL": { + "type": "string" + } + } } } } - } + ] } } } From 7ef52d8ed4de49579783ddd5c30a59e4e0f50c2d Mon Sep 17 00:00:00 2001 From: Samuel Oechsler Date: Wed, 30 Oct 2024 20:34:16 +0100 Subject: [PATCH 16/18] Update yarn.lock --- backend/yarn.lock | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/backend/yarn.lock b/backend/yarn.lock index 725168e1..eee0a79f 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1100,13 +1100,13 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" - encodeurl "~2.0.0" + encodeurl "~1.0.2" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -2342,6 +2342,13 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -2510,6 +2517,25 @@ semver@~7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -2578,7 +2604,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.6: +side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== From 1a030a6ddd022a64165a4ba5b5bf76370b968114 Mon Sep 17 00:00:00 2001 From: Samuel Oechsler Date: Wed, 30 Oct 2024 20:35:01 +0100 Subject: [PATCH 17/18] Enforce token auth for odic config PUT call --- backend/lib/express/jwt-decode.js | 11 ++++++++--- backend/routes/oidc.js | 10 +++++----- backend/routes/settings.js | 5 +++-- frontend/js/app/api.js | 7 +++++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/backend/lib/express/jwt-decode.js b/backend/lib/express/jwt-decode.js index 745763a7..193a3d0e 100644 --- a/backend/lib/express/jwt-decode.js +++ b/backend/lib/express/jwt-decode.js @@ -4,9 +4,14 @@ 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) + + // Allow unauthenticated access to get the oidc configuration + let oidc_access = + req.url === '/oidc-config' && + req.method === 'GET' && + !access.token.getUserId(); + + access.load(oidc_access) .then(() => { res.locals.access = access; next(); diff --git a/backend/routes/oidc.js b/backend/routes/oidc.js index 9c8030f9..751c04f5 100644 --- a/backend/routes/oidc.js +++ b/backend/routes/oidc.js @@ -1,11 +1,11 @@ const crypto = require('crypto'); -const error = require('../../lib/error'); +const error = require('../lib/error'); const express = require('express'); -const jwtdecode = require('../../lib/express/jwt-decode'); -const logger = require('../../logger').oidc; +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'); +const settingModel = require('../models/setting'); +const internalToken = require('../internal/token'); let router = express.Router({ caseSensitive: true, diff --git a/backend/routes/settings.js b/backend/routes/settings.js index d870974f..aa7d414e 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -72,13 +72,14 @@ router }) .then((row) => { if (row.id === 'oidc-config') { - // redact oidc configuration via api + // Redact oidc configuration via api (unauthenticated get call) 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 + + // Remove these temporary cookies used during oidc authentication res.clearCookie('npm_oidc'); res.clearCookie('npm_oidc_error'); } diff --git a/frontend/js/app/api.js b/frontend/js/app/api.js index 207cb548..03e787d7 100644 --- a/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -59,8 +59,11 @@ function fetch(verb, path, data, options) { }, beforeSend: function (xhr) { - // allow unauthenticated access to OIDC configuration - if (path === 'settings/oidc-config') return; + // Allow unauthenticated access to get the oidc configuration + if (path === 'settings/oidc-config' && verb === "get") { + return; + } + xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); }, From eb312cc61d122a84cf0647b2b0c100a3dfc1cf99 Mon Sep 17 00:00:00 2001 From: Samuel Oechsler Date: Thu, 31 Oct 2024 21:23:45 +0100 Subject: [PATCH 18/18] Remove nodemon dependency in package.json as it is already in devDependencies --- backend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index a13d7aa5..b29eff02 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,7 +21,6 @@ "moment": "^2.29.4", "mysql2": "^3.11.1", "node-rsa": "^1.0.8", - "nodemon": "^2.0.2", "openid-client": "^5.4.0", "objection": "3.0.1", "path": "^0.12.7",