From 99e0979c2ec473d79f10db2129b738676623e6cc Mon Sep 17 00:00:00 2001 From: Johannes Zellner Date: Fri, 24 Mar 2023 20:08:17 +0100 Subject: [PATCH] oidc: use better json based file store for objects --- dashboard/src/js/client.js | 9 ++ dashboard/src/views/profile.html | 5 +- dashboard/src/views/profile.js | 8 ++ src/oidc.js | 114 ++++++++++++++++++------- src/routes/index.js | 2 +- src/routes/{oidcclients.js => oidc.js} | 25 ++++-- src/server.js | 13 +-- 7 files changed, 129 insertions(+), 47 deletions(-) rename src/routes/{oidcclients.js => oidc.js} (89%) diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index 023e70d5b..940179255 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -1929,6 +1929,15 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }); }; + Client.prototype.destroyOidcSession = function (callback) { + del('/api/v1/oidc/sessions', null, function (error, data, status) { + if (error) return callback(error); + if (status !== 204) return callback(new ClientError(status, data)); + + callback(null); + }); + }; + Client.prototype.getOidcClients = function (callback) { get('/api/v1/oidc/clients', null, function (error, data, status) { if (error) return callback(error); diff --git a/dashboard/src/views/profile.html b/dashboard/src/views/profile.html index d7bd9289e..a112774b9 100644 --- a/dashboard/src/views/profile.html +++ b/dashboard/src/views/profile.html @@ -542,10 +542,9 @@

{{ 'profile.loginTokens.description' | tr:{ webadminTokenCount: tokens.webadminTokens.length, cliTokenCount: cliTokens.length } }}

- -
+
- + \ No newline at end of file diff --git a/dashboard/src/views/profile.js b/dashboard/src/views/profile.js index fe9c7dc40..f61a98721 100644 --- a/dashboard/src/views/profile.js +++ b/dashboard/src/views/profile.js @@ -709,6 +709,14 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans } }; + $scope.logoutFromAll = function () { + Client.destroyOidcSession(function (error) { + if (error) console.error('Failed to destroy oidc session', error); + + $scope.tokens.revokeAllWebAndCliTokens(); + }); + }; + Client.onReady(function () { $scope.appPassword.refresh(); $scope.tokens.refresh(); diff --git a/src/oidc.js b/src/oidc.js index 5c749c1cf..ac00289af 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -3,6 +3,7 @@ exports = module.exports = { start, stop, + revokeByUserId, clients: { add: clientsAdd, get: clientsGet, @@ -97,6 +98,69 @@ async function clientsList() { return results; } +// ----------------------------- +// Basic in-memory json file backed based data store +// ----------------------------- +const DATA_STORE = {}; + +function load(modelName) { + assert.strictEqual(typeof modelName, 'string'); + + if (DATA_STORE[modelName]) return; + + const filePath = path.join(paths.OIDC_STORE_DIR, `${modelName}.json`); + debug(`load: model ${modelName} based on ${filePath}.`); + + let data = {}; + try { + data = JSON.parse(fs.readFileSync(filePath), 'utf8'); + } catch (e) { + debug(`load: failed to read ${filePath}, start with new one.`, e); + } + + DATA_STORE[modelName] = data; +} + +function save(modelName) { + assert.strictEqual(typeof modelName, 'string'); + + if (!DATA_STORE[modelName]) return; + + const filePath = path.join(paths.OIDC_STORE_DIR, `${modelName}.json`); + debug(`save: model ${modelName} to ${filePath}.`); + + try { + fs.writeFileSync(filePath, JSON.stringify(DATA_STORE[modelName]), 'utf8'); + } catch (e) { + debug(`revokeByUserId: failed to write ${filePath}`, e); + } +} + +// ----------------------------- +// Session, Grant and Token management +// ----------------------------- +// This is based on the same storage as the below CloudronAdapter +async function revokeByUserId(userId) { + assert.strictEqual(typeof userId, 'string'); + + debug(`revokeByUserId: userId:${userId}`); + + function revokeObjects(modelName) { + load(modelName); + + for (let id in DATA_STORE[modelName]) { + if (DATA_STORE[modelName][id].payload?.accountId === userId) delete DATA_STORE[modelName][id]; + } + + save(modelName); + } + + revokeObjects('Session'); + revokeObjects('Grant'); + revokeObjects('AuthorizationCode'); + revokeObjects('AccessToken'); +} + // ----------------------------- // Generic oidc node module data store model // ----------------------------- @@ -115,22 +179,10 @@ class CloudronAdapter { constructor(name) { this.name = name; - if (this.name === 'Client') { - this.store = null; - this.fileStorePath = null; - } else { - this.fileStorePath = path.join(paths.OIDC_STORE_DIR, `${name}.json`); + debug(`Creating storage adapter for ${name}`); - debug(`Creating storage adapter for ${name} backed by ${this.fileStorePath}`); - - let data = {}; - try { - data = JSON.parse(fs.readFileSync(this.fileStorePath), 'utf8'); - } catch (e) { - debug(`filestore for adapter ${name} not found, start with new one`); - } - - this.store = data; + if (this.name !== 'Client') { + load(name); } } @@ -152,8 +204,8 @@ class CloudronAdapter { if (this.name === 'Client') { console.log('WARNING!! this should not happen as it is stored in our db'); } else { - this.store[id] = { id, expiresIn, payload, consumed: false }; - if (this.fileStorePath) fs.writeFileSync(this.fileStorePath, JSON.stringify(this.store), 'utf8'); + DATA_STORE[this.name][id] = { id, expiresIn, payload, consumed: false }; + save(this.name); } } @@ -191,11 +243,11 @@ class CloudronAdapter { return tmp; } else { - if (!this.store[id]) return null; + if (!DATA_STORE[this.name][id]) return null; - debug(`[${this.name}] find id:${id}`, this.store[id]); + debug(`[${this.name}] find id:${id}`, DATA_STORE[this.name][id]); - return this.store[id].payload; + return DATA_STORE[this.name][id].payload; } } @@ -230,8 +282,8 @@ class CloudronAdapter { if (this.name === 'Client') { console.log('WARNING!! this should not happen as it is stored in our db'); } else { - for (let d in this.store) { - if (this.store[d].payload.uid === uid) return this.store[d].payload; + for (let d in DATA_STORE[this.name]) { + if (DATA_STORE[this.name][d].payload.uid === uid) return DATA_STORE[this.name][d].payload; } return false; @@ -255,9 +307,8 @@ class CloudronAdapter { if (this.name === 'Client') { console.log('WARNING!! this should not happen as it is stored in our db'); } else { - if (this.store[id]) this.store[id].consumed = true; - - if (this.fileStorePath) fs.writeFileSync(this.fileStorePath, JSON.stringify(this.store), 'utf8'); + if (DATA_STORE[this.name][id]) DATA_STORE[this.name][id].consumed = true; + save(this.name); } } @@ -277,9 +328,8 @@ class CloudronAdapter { if (this.name === 'Client') { console.log('WARNING!! this should not happen as it is stored in our db'); } else { - delete this.store[id]; - - if (this.fileStorePath) fs.writeFileSync(this.fileStorePath, JSON.stringify(this.store), 'utf8'); + delete DATA_STORE[this.name][id]; + save(this.name); } } @@ -299,10 +349,10 @@ class CloudronAdapter { if (this.name === 'Client') { console.log('WARNING!! this should not happen as it is stored in our db'); } else { - for (let d in this.store) { - if (this.store[d].grantId === grantId) { - delete this.store[d]; - return; + for (let d in DATA_STORE[this.name]) { + if (DATA_STORE[this.name][d].grantId === grantId) { + delete DATA_STORE[this.name][d]; + return save(this.name); } } } diff --git a/src/routes/index.js b/src/routes/index.js index ec6c2a413..4581883be 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -17,7 +17,7 @@ exports = module.exports = { mailserver: require('./mailserver.js'), network: require('./network.js'), notifications: require('./notifications.js'), - oidcclients: require('./oidcclients.js'), + oidc: require('./oidc.js'), profile: require('./profile.js'), provision: require('./provision.js'), services: require('./services.js'), diff --git a/src/routes/oidcclients.js b/src/routes/oidc.js similarity index 89% rename from src/routes/oidcclients.js rename to src/routes/oidc.js index d5ad734fc..870f4548a 100644 --- a/src/routes/oidcclients.js +++ b/src/routes/oidc.js @@ -1,11 +1,15 @@ 'use strict'; exports = module.exports = { - get, - list, - add, - update, - remove + clients: { + get, + list, + add, + update, + del + }, + + destroyUserSession }; const assert = require('assert'), @@ -78,7 +82,7 @@ async function list(req, res, next) { next(new HttpSuccess(200, { clients: result })); } -async function remove(req, res, next) { +async function del(req, res, next) { assert.strictEqual(typeof req.params.clientId, 'string'); const [error] = await safe(oidc.clients.del(req.params.clientId)); @@ -86,3 +90,12 @@ async function remove(req, res, next) { next(new HttpSuccess(204)); } + +async function destroyUserSession(req, res, next) { + assert.strictEqual(typeof req.user, 'object'); + + const [error] = await safe(oidc.revokeByUserId(req.user.id)); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204)); +} diff --git a/src/server.js b/src/server.js index 5a9902561..685e2eb0d 100644 --- a/src/server.js +++ b/src/server.js @@ -369,11 +369,14 @@ async function initializeExpressSync() { router.get ('/well-known-handler/*', routes.wellknown.get); // 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); - router.post('/api/v1/oidc/clients/:clientId', json, token, authorizeAdmin, routes.oidcclients.update); - router.del ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidcclients.remove); + router.get ('/api/v1/oidc/clients', token, authorizeAdmin, routes.oidc.clients.list); + router.post('/api/v1/oidc/clients', json, token, authorizeAdmin, routes.oidc.clients.add); + router.get ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidc.clients.get); + router.post('/api/v1/oidc/clients/:clientId', json, token, authorizeAdmin, routes.oidc.clients.update); + router.del ('/api/v1/oidc/clients/:clientId', token, authorizeAdmin, routes.oidc.clients.del); + + // OpenID connect sessions + router.del ('/api/v1/oidc/sessions', token, authorizeUser, routes.oidc.destroyUserSession); // disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level // we rely on nginx for timeouts on the TCP level (see client_header_timeout)