From 7f5ed299842ba40d8fd63bf220f909b2a54db466 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 30 Jan 2025 17:48:32 +0000 Subject: [PATCH] Add support for httpNodeAuth --- .gitignore | 2 +- lib/launcher.js | 16 +- .../@flowfuse/flowfuse-auth/adminAuth.js | 80 ++++ .../flowfuse-auth/httpAuthMiddleware.js | 143 +++++++ .../@flowfuse/flowfuse-auth/httpAuthPlugin.js | 10 + .../@flowfuse/flowfuse-auth/package.json | 22 ++ .../@flowfuse/flowfuse-auth/strategy.js | 67 ++++ lib/template/template-settings.js | 22 +- package-lock.json | 351 +++++++++++++++++- package.json | 9 +- 10 files changed, 712 insertions(+), 10 deletions(-) create mode 100644 lib/plugins/node_modules/@flowfuse/flowfuse-auth/adminAuth.js create mode 100644 lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js create mode 100644 lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthPlugin.js create mode 100644 lib/plugins/node_modules/@flowfuse/flowfuse-auth/package.json create mode 100644 lib/plugins/node_modules/@flowfuse/flowfuse-auth/strategy.js diff --git a/.gitignore b/.gitignore index 0d144e7..a8d1b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -node_modules/ +/node_modules/ var/ diff --git a/lib/launcher.js b/lib/launcher.js index 679b703..64d338d 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -304,8 +304,22 @@ class Launcher { // The `httpNodeAuth` config is passed straight through to Node-RED // It is however sanitised in config.js to ensure it is an object // containing `user` and `pass` properties. - settings.httpNodeAuth = this.config.httpNodeAuth + settings.flowforge.httpNodeAuth = { + type: 'basic', + ...this.config.httpNodeAuth + } + } else if (this.settings?.security?.httpNodeAuth) { + settings.flowforge.httpNodeAuth = { + ...this.settings.security.httpNodeAuth, + bin: path.join(__dirname, 'plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js') + } + if (settings.flowforge.httpNodeAuth.type === 'ff-user') { + // Add the ff-auth plugin + settings.nodesDir = settings.nodesDir || [] + settings.nodesDir.push(path.join(__dirname, 'plugins', 'node_modules', '@flowfuse', 'flowfuse-auth')) + } } + await fs.writeFile(this.files.userSettings, JSON.stringify(settings)) } diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/adminAuth.js b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/adminAuth.js new file mode 100644 index 0000000..35c40c4 --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/adminAuth.js @@ -0,0 +1,80 @@ +const { OAuth2 } = require('oauth') +const { Strategy } = require('./strategy') + +module.exports = (options) => { + ['clientID', 'clientSecret', 'forgeURL', 'baseURL'].forEach(prop => { + if (!options[prop]) { + throw new Error(`Missing configuration option ${prop}`) + } + }) + + const clientID = options.clientID + const clientSecret = options.clientSecret + const forgeURL = options.forgeURL + const baseURL = new URL(options.baseURL) + let basePath = baseURL.pathname || '' + if (basePath.endsWith('/')) { + basePath = basePath.substring(0, basePath.length - 1) + } + const callbackURL = `${basePath}/auth/strategy/callback` + const authorizationURL = `${forgeURL}/account/authorize` + const tokenURL = `${forgeURL}/account/token` + const userInfoURL = `${forgeURL}/api/v1/user` + + const oa = new OAuth2(clientID, clientSecret, '', authorizationURL, tokenURL) + + const version = require('../../package.json').version + + const activeUsers = {} + + function addUser (username, profile, refreshToken, expiresIn) { + if (activeUsers[username]) { + clearTimeout(activeUsers[username].refreshTimeout) + } + activeUsers[username] = { + profile, + refreshToken, + expiresIn + } + activeUsers[username].refreshTimeout = setTimeout(function () { + oa.getOAuthAccessToken(refreshToken, { + grant_type: 'refresh_token' + }, function (err, accessToken, refreshToken, results) { + if (err) { + delete activeUsers[username] + } else { + addUser(username, profile, refreshToken, results.expires_in) + } + }) + }, expiresIn * 1000) + } + + return { + type: 'strategy', + strategy: { + name: 'FlowFuse', + autoLogin: true, + label: 'Sign in', + strategy: Strategy, + options: { + authorizationURL, + tokenURL, + callbackURL, + userInfoURL, + scope: `editor-${version}`, + clientID, + clientSecret, + pkce: true, + state: true, + verify: function (accessToken, refreshToken, params, profile, done) { + profile.permissions = [params.scope || 'read'] + addUser(profile.username, profile, refreshToken, params.expires_in) + done(null, profile) + } + } + }, + users: async function (username) { + return activeUsers[username] && activeUsers[username].profile + } + } +} diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js new file mode 100644 index 0000000..0afb16e --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js @@ -0,0 +1,143 @@ +const crypto = require('crypto') +const got = require('got') +const session = require('express-session') +const MemoryStore = require('memorystore')(session) +const { Passport } = require('passport') +const { Strategy } = require('./strategy') + +let options +let passport +let httpNodeApp +// let client +// const httpTokenCache = {} + +module.exports = { + init (_options) { + options = _options + console.log('init with options', options) + return [ + async (req, res, next) => { + try { + if (req.session.ffSession) { + next() + // Not support bearer token support for now + // } else if (req.get('Authorization')?.startsWith('Bearer')) { + // // We should include the Project ID and the path along with the token + // // to be checked to allow scoping tokens + // const token = req.get('Authorization').split(' ')[1] + // const cacheHit = httpTokenCache[token] + // if (cacheHit) { + // const age = (Date.now() - cacheHit.age) / 1000 + // if (age < 300) { + // next() + // return + // } + // delete httpTokenCache[token] + // } + // const query = { + // path: req.path + // } + // try { + // await client.get(options.projectId, { + // headers: { + // authorization: `Bearer ${token}` + // }, + // searchParams: query + // }) + // httpTokenCache[token] = { age: Date.now() } + // next() + // } catch (err) { + // // console.log(err) + // const error = new Error('Failed to check token') + // error.status = 401 + // next(error) + // } + } else { + console.log('auth required - but we have options') + req.session.redirectTo = req.originalUrl + passport.authenticate('FlowFuse', { session: false })(req, res, next) + } + } catch (err) { + console.log(err.stack) + throw err + } + }, + (err, req, res, next) => { + res.status(err.status).send() + } + ] + }, + + setupAuthRoutes (app) { + if (!options) { + // If `init` has not been called, then the ff-user auth type + // has not been selected. No need to setup any further routes. + return + } + // 'app' is RED.httpNode - the express app that handles all http routes + // exposed by the flows. + + passport = new Passport() + httpNodeApp = app + + httpNodeApp.use(session({ + // As the session is only used across the life-span of an auth + // hand-shake, we can use a instance specific random string + secret: crypto.randomBytes(20).toString('hex'), + resave: false, + saveUninitialized: false, + store: new MemoryStore({ + checkPeriod: 86400000 // prune expired entries every 24h + }) + })) + + app.use(passport.initialize()) + + // CallbackURL is set as a relative path - passport will prepend the appropriate + // hostname based on the request it is handling. + const callbackURL = '/_ffAuth/callback' + const authorizationURL = `${options.forgeURL}/account/authorize` + const tokenURL = `${options.forgeURL}/account/token` + const userInfoURL = `${options.forgeURL}/api/v1/user` + const version = require('../../../../../package.json').version + + passport.use('FlowFuse', new Strategy({ + authorizationURL, + tokenURL, + callbackURL, + userInfoURL, + scope: `httpAuth-${version}`, + clientID: options.clientID, + clientSecret: options.clientSecret, + pkce: true, + state: true + }, function (accessToken, refreshToken, params, profile, done) { + done(null, profile) + })) + + app.get('/_ffAuth/callback', passport.authenticate('FlowFuse', { + session: false + }), (req, res) => { + req.session.user = req.user + req.session.ffSession = true + if (req.session?.redirectTo) { + const redirectTo = req.session.redirectTo + delete req.session.redirectTo + res.redirect(redirectTo) + } else { + res.redirect('/') + } + }) + + // need to decide on the path here + client = got.extend({ + prefixUrl: `${options.forgeURL}/account/check/http`, + headers: { + 'user-agent': 'FlowFuse HTTP Node Auth' + }, + timeout: { + request: 500 + } + }) + } +} diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthPlugin.js b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthPlugin.js new file mode 100644 index 0000000..352689f --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthPlugin.js @@ -0,0 +1,10 @@ +const { setupAuthRoutes } = require('./httpAuthMiddleware') + +module.exports = (RED) => { + RED.plugins.registerPlugin('ff-auth-plugin', { + onadd: () => { + RED.log.info('FlowFuse HTTP Authentication Plugin loaded') + setupAuthRoutes(RED.httpNode) + } + }) +} diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/package.json b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/package.json new file mode 100644 index 0000000..acf72b9 --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/package.json @@ -0,0 +1,22 @@ +{ + "name": "@flowfuse/flowfuse-auth", + "version": "0.0.1", + "description": "FlowFuse auth plugin for Node-RED", + "keywords": [ + "node-red", + "flowfuse" + ], + "author": { + "name": "FlowFuse Inc." + }, + "license": "Apache-2.0", + "node-red": { + "version": ">=3.0.0", + "plugins": { + "flowfuse-auth": "httpAuthPlugin.js" + } + }, + "exports": { + "./authMiddleware": "./lib/auth/httpAuthMiddleware.js" + } +} \ No newline at end of file diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/strategy.js b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/strategy.js new file mode 100644 index 0000000..2544e95 --- /dev/null +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/strategy.js @@ -0,0 +1,67 @@ +const util = require('util') +const url = require('url') +const OAuth2Strategy = require('passport-oauth2') + +function Strategy (options, verify) { + this.options = options + this._base = Object.getPrototypeOf(Strategy.prototype) + this._base.constructor.call(this, this.options, verify) + this.name = 'FlowFuse' + this.isSecure = /^https:/.test(options.authorizationURL) + this.isRelativeCallback = !/^https?:/.test(options.callbackURL) +} + +util.inherits(Strategy, OAuth2Strategy) + +/** + * Patch the authenticate function so we can do per-request generation of the + * callback uri + */ +Strategy.prototype.__authenticate = Strategy.prototype.authenticate + +Strategy.prototype.authenticate = function (req, options) { + const strategyOptions = { ...options } + + if (this.isRelativeCallback) { + // Get the base url of the request + + // This logic comes from passport_oauth2/lib/utils - but we use our + // own check for whether to redirect to https or http based on the + // authorizationURL we've been provided + const app = req.app + let trustProxy = this._trustProxy + if (app && app.get && app.get('trust proxy')) { + trustProxy = true + } + const protocol = this.isSecure ? 'https' : 'http' + const host = (trustProxy && req.headers['x-forwarded-host']) || req.headers.host + const path = req.url || '' + const base = protocol + '://' + host + path + strategyOptions.callbackURL = (new url.URL(this.options.callbackURL, base)).toString() + } + + return this.__authenticate(req, strategyOptions) +} + +Strategy.prototype.userProfile = function (accessToken, done) { + this._oauth2.useAuthorizationHeaderforGET(true) + this._oauth2.get(this.options.userInfoURL, accessToken, (err, body) => { + if (err) { + return done(err) + } + try { + const json = JSON.parse(body) + done(null, { + username: json.username, + email: json.email, + image: json.avatar, + name: json.name, + userId: json.id + }) + } catch (e) { + done(e) + } + }) +} + +module.exports = { Strategy } diff --git a/lib/template/template-settings.js b/lib/template/template-settings.js index e01d886..efc49bc 100644 --- a/lib/template/template-settings.js +++ b/lib/template/template-settings.js @@ -199,11 +199,25 @@ if (settings.https) { if (settings.httpStatic) { runtimeSettings.httpStatic = settings.httpStatic } - -if (settings.httpNodeAuth && typeof settings.httpNodeAuth === 'object') { +if (settings.flowforge.httpNodeAuth?.user && settings.flowforge.httpNodeAuth?.pass) { runtimeSettings.httpNodeAuth = { - user: settings.httpNodeAuth.user, - pass: settings.httpNodeAuth.pass + user: settings.flowforge.httpNodeAuth.user, + pass: settings.flowforge.httpNodeAuth.pass + } +} else if (settings.flowforge.httpNodeAuth?.type === 'ff-user') { + const ffAuthMiddleware = require(settings.flowforge.httpNodeAuth.bin).init({ + type: 'flowforge-user', + // baseURL is the url of the http endpoints. We don't know the external + // ip/name of the device, so we use a placeholder. The code only needs the + // pathname (ie '/') from this URL - the domain/etc is not used. + baseURL: 'https://example.com/', + forgeURL: settings.flowforge.forgeURL, + clientID: settings.flowforge.httpNodeAuth.clientID, + clientSecret: settings.flowforge.httpNodeAuth.clientSecret + }) + runtimeSettings.httpNodeMiddleware = ffAuthMiddleware + runtimeSettings.ui = { + middleware: ffAuthMiddleware } } module.exports = runtimeSettings diff --git a/package-lock.json b/package-lock.json index 3942504..3af97eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,25 @@ { "name": "@flowfuse/device-agent", - "version": "3.0.2", + "version": "3.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@flowfuse/device-agent", - "version": "3.0.2", + "version": "3.1.0", "license": "Apache-2.0", "dependencies": { "@flowfuse/nr-theme": "^1.8.0", "command-line-args": "^5.2.1", "command-line-usage": "^6.1.3", + "express-session": "^1.18.0", "got": "^11.8.6", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.4", + "memorystore": "^1.6.7", "mqtt": "^5.10.1", + "passport": "0.6.0", + "passport-oauth2": "^1.6.1", "proxy-from-env": "^1.1.0", "semver": "^7.3.8", "ws": "^8.13.0", @@ -710,6 +714,14 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-auth-parser": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2-1.tgz", @@ -1037,6 +1049,19 @@ "typedarray": "^0.0.6" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1143,6 +1168,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -1864,6 +1897,37 @@ "node": ">=0.8.x" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3014,6 +3078,32 @@ "node": ">=10" } }, + "node_modules/memorystore": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz", + "integrity": "sha512-OZnmNY/NDrKohPQ+hxp0muBcBKrzKNtHr55DbqSx9hLsYVNnomSAMRAtI7R64t3gf3ID7tHQA7mG4oL3Hu9hdw==", + "dependencies": { + "debug": "^4.3.0", + "lru-cache": "^4.0.3" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/memorystore/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/memorystore/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3324,6 +3414,11 @@ "js-sdsl": "4.3.0" } }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -3381,6 +3476,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3456,6 +3559,58 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3498,6 +3653,11 @@ "isarray": "0.0.1" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3557,6 +3717,11 @@ "integrity": "sha512-SjXgyBmr0dBbKUZ0jOzp0N9urTcDOI1cd1oEeE43W1vG4OMwYYLggCRcMJ0zv0gdTA8Imb4cAiYj8Ic/PWv1mw==", "dev": true }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -3615,6 +3780,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4242,6 +4415,22 @@ "node": ">=8" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -4272,6 +4461,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -5066,6 +5263,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "basic-auth-parser": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2-1.tgz", @@ -5320,6 +5522,16 @@ "typedarray": "^0.0.6" } }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5387,6 +5599,11 @@ "object-keys": "^1.1.1" } }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -5908,6 +6125,36 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, + "express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "requires": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6742,6 +6989,31 @@ "yallist": "^4.0.0" } }, + "memorystore": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz", + "integrity": "sha512-OZnmNY/NDrKohPQ+hxp0muBcBKrzKNtHr55DbqSx9hLsYVNnomSAMRAtI7R64t3gf3ID7tHQA7mG4oL3Hu9hdw==", + "requires": { + "debug": "^4.3.0", + "lru-cache": "^4.0.3" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -6993,6 +7265,11 @@ "js-sdsl": "4.3.0" } }, + "oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -7032,6 +7309,11 @@ "es-abstract": "^1.20.4" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7086,6 +7368,38 @@ "callsites": "^3.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7119,6 +7433,11 @@ "isarray": "0.0.1" } }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -7163,6 +7482,11 @@ "integrity": "sha512-SjXgyBmr0dBbKUZ0jOzp0N9urTcDOI1cd1oEeE43W1vG4OMwYYLggCRcMJ0zv0gdTA8Imb4cAiYj8Ic/PWv1mw==", "dev": true }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7195,6 +7519,11 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7657,6 +7986,19 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -7684,6 +8026,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, "uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", diff --git a/package.json b/package.json index b67939b..eed64ba 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "@flowfuse/device-agent", - "version": "3.0.2", + "version": "3.1.0", "description": "An Edge Agent for running Node-RED instances deployed from the FlowFuse Platform", "exports": { "./libraryPlugin": "./lib/plugins/libraryPlugin.js", - "./auditLogger": "./lib/auditLogger/index.js" + "./auditLogger": "./lib/auditLogger/index.js", + "./adminAuth": "./lib/auth/adminAuth.js" }, "main": "index.js", "repository": { @@ -37,10 +38,14 @@ "@flowfuse/nr-theme": "^1.8.0", "command-line-args": "^5.2.1", "command-line-usage": "^6.1.3", + "express-session": "^1.18.0", "got": "^11.8.6", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.4", + "memorystore": "^1.6.7", "mqtt": "^5.10.1", + "passport": "0.6.0", + "passport-oauth2": "^1.6.1", "proxy-from-env": "^1.1.0", "semver": "^7.3.8", "ws": "^8.13.0",