diff --git a/box.js b/box.js index d1298bf01..9f381c736 100755 --- a/box.js +++ b/box.js @@ -4,6 +4,7 @@ const fs = require('fs'), ldap = require('./src/ldap.js'), + oidc = require('./src/oidc.js'), paths = require('./src/paths.js'), proxyAuth = require('./src/proxyauth.js'), safe = require('safetydance'), @@ -37,6 +38,7 @@ async function startServers() { await server.start(); // do this first since it also inits the database await proxyAuth.start(); await ldap.start(); + await oidc.start(); const conf = await settings.getDirectoryServerConfig(); if (conf.enabled) await directoryServer.start(); diff --git a/package.json b/package.json index 5356f2727..775e48bfa 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "debug": "^4.3.4", "dockerode": "^3.3.4", "ejs": "^3.1.8", + "eslint": "^8.36.0", "express": "^4.18.2", "ipaddr.js": "^2.0.1", "jsdom": "^21.1.0", diff --git a/src/constants.js b/src/constants.js index 35743defc..df4c7357a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -30,6 +30,7 @@ exports = module.exports = { LDAP_PORT: 3002, DOCKER_PROXY_PORT: 3003, USER_DIRECTORY_LDAPS_PORT: 3004, // user directory LDAP with TLS rerouting in iptables, public port is 636 + OIDC_PORT: 3005, // docker IPs DOCKER_IPv4_SUBNET: '172.18.0.0/16', diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index be29caec1..29c74681e 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -255,6 +255,11 @@ server { client_max_body_size 0; } + location ~ ^/openid/ { + proxy_pass http://127.0.0.1:3005; + client_max_body_size 2m; + } + # graphite paths (uncomment block below and visit /graphite-web/) # remember to comment out the CSP policy as well to access the graphite dashboard # location ~ ^/graphite-web/ { diff --git a/src/oidc.js b/src/oidc.js index cedbcf68d..1d869549b 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -1,7 +1,6 @@ 'use strict'; exports = module.exports = { - getProvider, start, stop, clients: { @@ -37,6 +36,10 @@ const assert = require('assert'), const OIDC_CLIENTS_TABLE_NAME = 'oidcClients'; const OIDC_CLIENTS_FIELDS = [ 'id', 'secret', 'loginRedirectUri', 'logoutRedirectUri' ]; +const ROUTE_PREFIX = '/openid'; + +let gHttpServer = null; + async function clientsAdd(id, secret, loginRedirectUri, logoutRedirectUri) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof secret, 'string'); @@ -286,8 +289,7 @@ class CloudronAdapter { } } -function renderInteractionPage(routePrefix, provider) { - assert.strictEqual(typeof routePrefix, 'string'); +function renderInteractionPage(provider) { assert.strictEqual(typeof provider, 'object'); return async function (req, res, next) { @@ -303,7 +305,7 @@ function renderInteractionPage(routePrefix, provider) { case 'login': { return res.render('login', { client, - submitUrl: `${routePrefix}/interaction/${uid}/login`, + submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/login`, uid, details: prompt.details, params, @@ -318,7 +320,7 @@ function renderInteractionPage(routePrefix, provider) { case 'consent': { return res.render('interaction', { client, - submitUrl: `${routePrefix}/interaction/${uid}/confirm`, + submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/confirm`, uid, details: prompt.details, params, @@ -515,8 +517,10 @@ async function postLogoutSuccessSource(ctx) { ctx.body = ejs.render(fs.readFileSync(path.join(__dirname, 'oidc_templates/post_logout.ejs'), 'utf8'), data, {}); } -async function getProvider(routePrefix) { - assert.strictEqual(typeof routePrefix, 'string'); +async function start() { + const app = express(); + + gHttpServer = http.createServer(app); const { Provider } = await import('oidc-provider'); @@ -532,7 +536,7 @@ async function getProvider(routePrefix) { adapter: CloudronAdapter, interactions: { url: async function(ctx, interaction) { - return `${routePrefix}/interaction/${interaction.uid}`; + return `${ROUTE_PREFIX}/interaction/${interaction.uid}`; } }, claims: { @@ -569,16 +573,35 @@ async function getProvider(routePrefix) { } }; - const provider = new Provider(`https://${settings.dashboardFqdn()}${routePrefix}`, configuration); + debug(`start: create provider for ${settings.dashboardFqdn()} at ${ROUTE_PREFIX}`); + const provider = new Provider(`https://${settings.dashboardFqdn()}${ROUTE_PREFIX}`, configuration); + + app.enable('trust proxy'); provider.proxy = true - return provider; -} + app.set('views', path.join(__dirname, 'oidc_templates')); + app.set('view engine', 'ejs'); -async function start() { + const json = middleware.json({ strict: true, limit: QUERY_LIMIT }); + function setNoCache(req, res, next) { + res.set('cache-control', 'no-store'); + next(); + } + app.get (`${ROUTE_PREFIX}/interaction/:uid`, setNoCache, routes.renderInteractionPage(provider)); + app.post(`${ROUTE_PREFIX}/interaction/:uid/login`, setNoCache, json, routes.interactionLogin(provider)); + app.post(`${ROUTE_PREFIX}/interaction/:uid/confirm`, setNoCache, json, routes.interactionConfirm(provider)); + app.get (`${ROUTE_PREFIX}/interaction/:uid/abort`, setNoCache, routes.interactionAbort(provider)); + + app.use(ROUTE_PREFIX, provider.callback()); + + await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.OIDC_PORT, '127.0.0.1'); } async function stop() { + if (!gHttpServer) return; + await util.promisify(gHttpServer.close.bind(gHttpServer))(); + + gHttpServer = null; } diff --git a/src/server.js b/src/server.js index d7f4edddf..968269c7a 100644 --- a/src/server.js +++ b/src/server.js @@ -14,7 +14,6 @@ const assert = require('assert'), express = require('express'), http = require('http'), middleware = require('./middleware'), - oidc = require('./oidc.js'), path = require('path'), routes = require('./routes/index.js'), safe = require('safetydance'), @@ -39,11 +38,6 @@ async function initializeExpressSync() { const json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded - function setNoCache(req, res, next) { - res.set('cache-control', 'no-store'); - next(); - } - app.set('json spaces', 2); // pretty json // for rate limiting @@ -375,19 +369,7 @@ async function initializeExpressSync() { // well known router.get ('/well-known-handler/*', routes.wellknown.get); - // OpenID connect - const oidcPrefix = '/api/v1/oidc'; - const oidcProvider = await oidc.getProvider(oidcPrefix); - app.set('views', path.join(__dirname, 'oidc_templates')); - app.set('view engine', 'ejs'); - - router.get ('/api/v1/oidc/interaction/:uid', setNoCache, oidc.routes.renderInteractionPage(oidcPrefix, oidcProvider)); - router.post('/api/v1/oidc/interaction/:uid/login', setNoCache, json, oidc.routes.interactionLogin(oidcProvider)); - router.post('/api/v1/oidc/interaction/:uid/confirm', setNoCache, json, oidc.routes.interactionConfirm(oidcProvider)); - router.get ('/api/v1/oidc/interaction/:uid/abort', setNoCache, oidc.routes.interactionAbort(oidcProvider)); - - app.use(oidcPrefix, oidcProvider.callback()); - + // OpenID connect clients router.get ('/api/v1/oidc/clients', token, authorizeAdmin, routes.oidcclients.list); router.post('/api/v1/oidc/clients', json, token, authorizeAdmin, routes.oidcclients.add); router.get ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidcclients.get); diff --git a/src/wellknown.js b/src/wellknown.js index afe7015aa..cd25433a8 100644 --- a/src/wellknown.js +++ b/src/wellknown.js @@ -41,7 +41,7 @@ async function get(domain, location) { } else if (location === 'openid-configuration') { // the oidc-provider module does not expose this in javascript but only via a route handler // we have to use the external route even - const [error, result] = await safe(superagent.get(`https://${settings.dashboardFqdn()}/api/v1/oidc/.well-known/openid-configuration`)); + const [error, result] = await safe(superagent.get(`https://${settings.dashboardFqdn()}/openid/.well-known/openid-configuration`)); if (error) return new BoxError(BoxError.INTERNAL_ERROR, 'unable to connect to internal OpenID routes'); return { type: 'application/json', body: result.body };