diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index f7f294145..f25093f5a 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -303,7 +303,7 @@ server { proxy_set_header Content-Length ""; } - location ~ ^/(login|logout)$ { + location ~ ^/(login|logout|callback)$ { proxy_pass http://127.0.0.1:3001; } @@ -314,7 +314,7 @@ server { if ($http_user_agent ~* "container") { return 401; } - return 302 /login?redirect=$request_uri; + return 302 "https://<%= proxyAuth.oidcEndpoint %>/openid/auth?client_id=<%= proxyAuth.oidcClientId %>&scope=openid profile email&response_type=code&redirect_uri=https://<%= vhost %>/callback"; } location <%= proxyAuth.location %> { diff --git a/src/oidc.js b/src/oidc.js index ac9eb5261..47de91322 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -4,6 +4,8 @@ exports = module.exports = { start, stop, revokeByUserId, + getUserByAuthCode, + consumeAuthCode, clients: { add: clientsAdd, get: clientsGet, @@ -197,6 +199,28 @@ async function revokeByUserId(userId) { revokeObjects('AccessToken'); } +async function consumeAuthCode(authCode) { + assert.strictEqual(typeof authCode, 'string'); + + const authData = DATA_STORE['AuthorizationCode'][authCode]; + if (!authData || !authData.payload) return; + + DATA_STORE['AuthorizationCode'][authCode].consumed = true; + + save('AuthorizationCode'); +} + +async function getUserByAuthCode(authCode) { + assert.strictEqual(typeof authCode, 'string'); + + load('AuthorizationCode'); + const authData = DATA_STORE['AuthorizationCode'][authCode]; + + if (!authData || !authData.payload || !authData.payload.accountId) return null; + + return await users.get(authData.payload.accountId); +} + // ----------------------------- // Generic oidc node module data store model // ----------------------------- diff --git a/src/proxyauth.js b/src/proxyauth.js index b80a0f267..7ecdc983b 100644 --- a/src/proxyauth.js +++ b/src/proxyauth.js @@ -22,6 +22,7 @@ const apps = require('./apps.js'), HttpSuccess = require('connect-lastmile').HttpSuccess, jwt = require('jsonwebtoken'), middleware = require('./middleware'), + oidc = require('./oidc.js'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), @@ -152,6 +153,19 @@ function auth(req, res, next) { next(new HttpSuccess(200, {})); } +async function callback(req, res, next) { + if (!req.query.code) return next(new HttpError(400, 'missing query argument "code"')); + + debug(`callback: with code ${req.query.code}`); + + req.user = await oidc.getUserByAuthCode(req.query.code); + + // this is one-time use + await oidc.consumeAuthCode(req.query.code); + + next(); +} + // endpoint called by login page, username and password posted as JSON body async function passwordAuth(req, res, next) { assert.strictEqual(typeof req.body, 'object'); @@ -226,6 +240,7 @@ function initializeAuthwallExpressSync() { .use(router) .use(middleware.lastMile()); + router.get ('/callback', callback, authorize); router.get ('/login', loginPage); router.get ('/auth', jwtVerify, authorizationHeader, auth); // called by nginx before accessing protected page router.post('/login', json, passwordAuth, authorize); diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 3a53c8e82..5f6c9e3bf 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -544,6 +544,11 @@ async function writeAppLocationNginxConfig(app, location, certificatePath) { }; data.ip = app.containerIp; data.port = app.manifest.httpPort; + + if (data.proxyAuth.enabled) { + data.proxyAuth.oidcClientId = app.id; + data.proxyAuth.oidcEndpoint = (await dashboard.getLocation()).fqdn; + } } else if (type === Location.TYPE_SECONDARY) { data.ip = app.containerIp; const secondaryDomain = app.secondaryDomains.find(sd => sd.fqdn === fqdn); diff --git a/src/services.js b/src/services.js index 6b024e69d..f449ab068 100644 --- a/src/services.js +++ b/src/services.js @@ -1737,6 +1737,25 @@ async function setupProxyAuth(app, options) { const env = [ { name: 'CLOUDRON_PROXY_AUTH', value: '1' } ]; await addonConfigs.set(app.id, 'proxyauth', env); + + debug('Creating OpenID client for proxyAuth'); + + // openid client_id is appId for now + const [error, result] = await safe(oidc.clients.get(app.id)); + if (error) throw error; + + // ensure we keep the secret + const data = { + secret: result ? result.secret : hat(4 * 128), + loginRedirectUri: `https://${app.fqdn}/callback`, + logoutRedirectUri: '', + tokenSignatureAlgorithm: 'RS256', + name: '', + appId: app.id + }; + + if (result) await oidc.clients.update(app.id, data); + else await oidc.clients.add(app.id, data); } async function teardownProxyAuth(app, options) { @@ -1744,6 +1763,11 @@ async function teardownProxyAuth(app, options) { assert.strictEqual(typeof options, 'object'); await addonConfigs.unset(app.id, 'proxyauth'); + + debug('Deleting OpenID client for proxyAuth'); + + const [error] = await safe(oidc.clients.del(app.id)); + if (error && error.reason !== BoxError.NOT_FOUND) throw error; } async function setupDocker(app, options) {