From ffc140096ccc6e61bb0ee30e3b031b0b455429c7 Mon Sep 17 00:00:00 2001 From: Bill Church Date: Wed, 19 May 2021 19:23:22 +0000 Subject: [PATCH] feat: move authentication system to pasport.js --- README.md | 4 +- app/package-lock.json | 37 +++++++- app/package.json | 5 +- app/server/app.js | 196 +++++++++++++++++++++++++++--------------- app/server/util.js | 46 ---------- 5 files changed, 168 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 395c89c..9699443 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ -[![GitHub version](https://badge.fury.io/gh/billchurch%2Fwebssh2.svg)](https://badge.fury.io/gh/billchurch%2Fwebssh2) +[![GitHub version](https://badge.fury.io/gh/billchurch%2Fwebssh2.svg)](https://badge.fury.io/gh/billchurch%2Fwebssh2) [![Build Status](https://travis-ci.org/billchurch/webssh2.svg?branch=master)](https://travis-ci.org/billchurch/webssh2) + + [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/billchurch) diff --git a/app/package-lock.json b/app/package-lock.json index b73f08f..feaff6c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "webssh2", - "version": "0.4.0", + "version": "0.5.0-dev-0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6332,6 +6332,36 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-http": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/passport-http/-/passport-http-0.3.0.tgz", + "integrity": "sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -6393,6 +6423,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "peek-stream": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", diff --git a/app/package.json b/app/package.json index 28a5446..a32e2f7 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "webssh2", - "version": "0.4.0", + "version": "0.5.0-dev-0", "ignore": [ ".gitignore" ], @@ -39,6 +39,9 @@ "express": "~4.17.1", "express-session": "~1.17.1", "morgan": "~1.10.0", + "passport": "^0.4.1", + "passport-custom": "^1.1.1", + "passport-http": "^0.3.0", "read-config-ng": "^3.0.2", "serve-favicon": "^2.5.0", "socket.io": "^4.1.1", diff --git a/app/server/app.js b/app/server/app.js index 16cddb8..ef707cd 100644 --- a/app/server/app.js +++ b/app/server/app.js @@ -6,11 +6,17 @@ // eslint-disable-next-line import/order const config = require('./config'); const path = require('path'); +const debug = require('debug')('WebSSH2'); +require('colors'); +// allow for color property extensions in log messages const nodeRoot = path.dirname(require.main.filename); const publicPath = path.join(nodeRoot, 'client', 'public'); const express = require('express'); const logger = require('morgan'); +const passport = require('passport'); +const { BasicStrategy } = require('passport-http'); +const CustomStrategy = require('passport-custom').Strategy; const app = express(); const server = require('http').Server(app); @@ -24,7 +30,7 @@ const io = require('socket.io')(server, { const session = require('express-session')({ secret: config.session.secret, name: config.session.name, - resave: true, + resave: false, saveUninitialized: false, unset: 'destroy', }); @@ -32,11 +38,38 @@ const appSocket = require('./socket'); const expressOptions = require('./expressOptions'); const myutil = require('./util'); -myutil.setDefaultCredentials( - config.user.name, - config.user.password, - config.user.privatekey, - config.user.overridebasic +// Static credentials strategy +// when config.user.overridebasic is true, those credentials +// are used instead of HTTP basic auth. +passport.use( + 'custom', + new CustomStrategy((req, done) => { + if (config.user.overridebasic) { + const user = { + username: config.user.name, + password: config.user.password, + privatekey: config.user.privatekey, + }; + return done(null, user); + } + return done(null, false); + }) +); + +// Basic auth strategy +passport.use( + new BasicStrategy((username, password, done) => { + const user = { + username, + password, + }; + debug( + `myAuth.name: ${username.yellow.bold.underline} and password ${ + password ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold + }` + ); + return done(null, user); + }) ); // safe shutdown @@ -65,7 +98,8 @@ module.exports = { server, config }; // express app.use(safeShutdownGuard); app.use(session); -app.use(myutil.basicAuth); +app.use(passport.initialize()); +app.use(passport.session()); if (config.accesslog) app.use(logger('common')); app.disable('x-powered-by'); @@ -75,8 +109,12 @@ app.use('/ssh', express.static(publicPath, expressOptions)); // favicon from root if being pre-fetched by browser to prevent a 404 app.use(favicon(path.join(publicPath, 'favicon.ico'))); +// this is currently broken due to the way passport works with Basic Auth... +// maybe this should never have worked in the first place app.get('/ssh/reauth', (req, res) => { const r = req.headers.referer || '/'; + req.logout(); + req.session.destroy(); res .status(401) .send( @@ -84,72 +122,88 @@ app.get('/ssh/reauth', (req, res) => { ); }); -// eslint-disable-next-line complexity -app.get('/ssh/host/:host?', (req, res) => { - res.sendFile(path.join(path.join(publicPath, 'client.htm'))); - // capture, assign, and validate variables - req.session.ssh = { - host: - config.ssh.host || - (validator.isIP(`${req.params.host}`) && req.params.host) || - (validator.isFQDN(req.params.host) && req.params.host) || - (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.params.host) && req.params.host), - port: - (validator.isInt(`${req.query.port}`, { min: 1, max: 65535 }) && req.query.port) || - config.ssh.port, - localAddress: config.ssh.localAddress, - localPort: config.ssh.localPort, - header: { - name: req.query.header || config.header.text, - background: req.query.headerBackground || config.header.background, - }, - algorithms: config.algorithms, - keepaliveInterval: config.ssh.keepaliveInterval, - keepaliveCountMax: config.ssh.keepaliveCountMax, - allowedSubnets: config.ssh.allowedSubnets, - term: - (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) && req.query.sshterm) || - config.ssh.term, - terminal: { - cursorBlink: validator.isBoolean(`${req.query.cursorBlink}`) - ? myutil.parseBool(req.query.cursorBlink) - : config.terminal.cursorBlink, - scrollback: - validator.isInt(`${req.query.scrollback}`, { min: 1, max: 200000 }) && req.query.scrollback - ? req.query.scrollback - : config.terminal.scrollback, - tabStopWidth: - validator.isInt(`${req.query.tabStopWidth}`, { min: 1, max: 100 }) && req.query.tabStopWidth - ? req.query.tabStopWidth - : config.terminal.tabStopWidth, - bellStyle: - req.query.bellStyle && ['sound', 'none'].indexOf(req.query.bellStyle) > -1 - ? req.query.bellStyle - : config.terminal.bellStyle, - }, - allowreplay: - config.options.challengeButton || - (validator.isBoolean(`${req.headers.allowreplay}`) - ? myutil.parseBool(req.headers.allowreplay) - : false), - allowreauth: config.options.allowreauth || false, - mrhsession: - validator.isAlphanumeric(`${req.headers.mrhsession}`) && req.headers.mrhsession - ? req.headers.mrhsession - : 'none', - serverlog: { - client: config.serverlog.client || false, - server: config.serverlog.server || false, - }, - readyTimeout: - (validator.isInt(`${req.query.readyTimeout}`, { min: 1, max: 300000 }) && - req.query.readyTimeout) || - config.ssh.readyTimeout, - }; - if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name); - if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background); +passport.serializeUser((user, done) => { + done(null, user); }); +passport.deserializeUser((user, done) => { + done(null, user); +}); + +// eslint-disable-next-line complexity +app.get( + '/ssh/host/:host?', + passport.authenticate(['custom', 'basic'], { session: true }), + (req, res) => { + req.session.username = req.user.username; + req.session.userpassword = req.user.password; + res.sendFile(path.join(path.join(publicPath, 'client.htm'))); + // capture, assign, and validate variables + req.session.ssh = { + host: + config.ssh.host || + (validator.isIP(`${req.params.host}`) && req.params.host) || + (validator.isFQDN(req.params.host) && req.params.host) || + (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.params.host) && req.params.host), + port: + (validator.isInt(`${req.query.port}`, { min: 1, max: 65535 }) && req.query.port) || + config.ssh.port, + localAddress: config.ssh.localAddress, + localPort: config.ssh.localPort, + header: { + name: req.query.header || config.header.text, + background: req.query.headerBackground || config.header.background, + }, + algorithms: config.algorithms, + keepaliveInterval: config.ssh.keepaliveInterval, + keepaliveCountMax: config.ssh.keepaliveCountMax, + allowedSubnets: config.ssh.allowedSubnets, + term: + (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) && req.query.sshterm) || + config.ssh.term, + terminal: { + cursorBlink: validator.isBoolean(`${req.query.cursorBlink}`) + ? myutil.parseBool(req.query.cursorBlink) + : config.terminal.cursorBlink, + scrollback: + validator.isInt(`${req.query.scrollback}`, { min: 1, max: 200000 }) && + req.query.scrollback + ? req.query.scrollback + : config.terminal.scrollback, + tabStopWidth: + validator.isInt(`${req.query.tabStopWidth}`, { min: 1, max: 100 }) && + req.query.tabStopWidth + ? req.query.tabStopWidth + : config.terminal.tabStopWidth, + bellStyle: + req.query.bellStyle && ['sound', 'none'].indexOf(req.query.bellStyle) > -1 + ? req.query.bellStyle + : config.terminal.bellStyle, + }, + allowreplay: + config.options.challengeButton || + (validator.isBoolean(`${req.headers.allowreplay}`) + ? myutil.parseBool(req.headers.allowreplay) + : false), + allowreauth: config.options.allowreauth || false, + mrhsession: + validator.isAlphanumeric(`${req.headers.mrhsession}`) && req.headers.mrhsession + ? req.headers.mrhsession + : 'none', + serverlog: { + client: config.serverlog.client || false, + server: config.serverlog.server || false, + }, + readyTimeout: + (validator.isInt(`${req.query.readyTimeout}`, { min: 1, max: 300000 }) && + req.query.readyTimeout) || + config.ssh.readyTimeout, + }; + if (req.session.ssh.header.name) validator.escape(req.session.ssh.header.name); + if (req.session.ssh.header.background) validator.escape(req.session.ssh.header.background); + } +); + // express error handling app.use((req, res) => { res.status(404).send("Sorry, can't find that!"); diff --git a/app/server/util.js b/app/server/util.js index e401640..5c56c67 100644 --- a/app/server/util.js +++ b/app/server/util.js @@ -2,52 +2,6 @@ // util.js // private -require('colors'); // allow for color property extensions in log messages -const debug = require('debug')('WebSSH2'); -const Auth = require('basic-auth'); - -const defaultCredentials = { username: null, password: null, privatekey: null }; - -exports.setDefaultCredentials = function setDefaultCredentials( - username, - password, - privatekey, - overridebasic -) { - defaultCredentials.username = username; - defaultCredentials.password = password; - defaultCredentials.privatekey = privatekey; - defaultCredentials.overridebasic = overridebasic; -}; - -exports.basicAuth = function basicAuth(req, res, next) { - const myAuth = Auth(req); - // If Authorize: Basic header exists and the password isn't blank - // AND config.user.overridebasic is false, extract basic credentials - // from client - if (myAuth && myAuth.pass !== '' && !defaultCredentials.overridebasic) { - req.session.username = myAuth.name; - req.session.userpassword = myAuth.pass; - debug( - `myAuth.name: ${myAuth.name.yellow.bold.underline} and password ${ - myAuth.pass ? 'exists'.yellow.bold.underline : 'is blank'.underline.red.bold - }` - ); - } else { - req.session.username = defaultCredentials.username; - req.session.userpassword = defaultCredentials.password; - req.session.privatekey = defaultCredentials.privatekey; - } - if (!req.session.userpassword && !req.session.privatekey) { - res.statusCode = 401; - debug('basicAuth credential request (401)'); - res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH"'); - res.end('Username and password required for web SSH service.'); - return; - } - next(); -}; - // takes a string, makes it boolean (true if the string is true, false otherwise) exports.parseBool = function parseBool(str) { return str.toLowerCase() === 'true';