feat: Add session-based authentication for SSH connections using HTTP Basic auth and express.js

This commit is contained in:
Bill Church 2024-07-18 17:13:23 +00:00
parent fe7248e056
commit afe462b180
No known key found for this signature in database
7 changed files with 240 additions and 36 deletions

View file

@ -7,6 +7,8 @@ const express = require('express')
const socketIo = require('socket.io') const socketIo = require('socket.io')
const path = require('path') const path = require('path')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
const session = require('express-session')
const sharedsession = require("express-socket.io-session")
const config = require('./config') const config = require('./config')
const socketHandler = require('./socket') const socketHandler = require('./socket')
const sshRoutes = require('./routes') const sshRoutes = require('./routes')
@ -16,10 +18,19 @@ const sshRoutes = require('./routes')
* @returns {express.Application} The Express application instance * @returns {express.Application} The Express application instance
*/ */
function createApp() { function createApp() {
var app = express(); const app = express();
// Resolve the correct path to the webssh2_client module // Resolve the correct path to the webssh2_client module
var clientPath = path.resolve(__dirname, '..', 'node_modules', 'webssh2_client', 'client', 'public'); const clientPath = path.resolve(__dirname, '..', 'node_modules', 'webssh2_client', 'client', 'public');
// Set up session middleware
const sessionMiddleware = session({
secret: config.session.secret || 'webssh2_secret',
resave: false,
saveUninitialized: true,
name: config.session.name || 'webssh2.sid'
});
app.use(sessionMiddleware);
// Handle POST and GET parameters // Handle POST and GET parameters
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
@ -31,7 +42,7 @@ function createApp() {
// Use the SSH routes // Use the SSH routes
app.use('/ssh', sshRoutes); app.use('/ssh', sshRoutes);
return app; return { app, sessionMiddleware };
} }
/** /**
@ -46,13 +57,21 @@ function createServer(app) {
/** /**
* Configures Socket.IO with the given server * Configures Socket.IO with the given server
* @param {http.Server} server - The HTTP server instance * @param {http.Server} server - The HTTP server instance
* @param {Function} sessionMiddleware - The session middleware
* @returns {import('socket.io').Server} The Socket.IO server instance * @returns {import('socket.io').Server} The Socket.IO server instance
*/ */
function configureSocketIO(server) { function configureSocketIO(server, sessionMiddleware) {
return socketIo(server, { const io = socketIo(server, {
path: '/ssh/socket.io', path: '/ssh/socket.io',
cors: getCorsConfig() cors: getCorsConfig()
}) });
// Share session with io sockets
io.use(sharedsession(sessionMiddleware, {
autoSave: true
}));
return io;
} }
/** /**
@ -80,9 +99,9 @@ function setupSocketIOListeners(io) {
* @returns {Object} An object containing the server, io, and app instances * @returns {Object} An object containing the server, io, and app instances
*/ */
function startServer() { function startServer() {
const app = createApp() const { app, sessionMiddleware } = createApp()
const server = createServer(app) const server = createServer(app)
const io = configureSocketIO(server) const io = configureSocketIO(server, sessionMiddleware)
// Set up Socket.IO listeners // Set up Socket.IO listeners
setupSocketIOListeners(io) setupSocketIOListeners(io)

View file

@ -6,6 +6,7 @@ const path = require('path')
const fs = require('fs') const fs = require('fs')
const readConfig = require('read-config-ng') const readConfig = require('read-config-ng')
const Ajv = require('ajv') const Ajv = require('ajv')
const crypto = require('crypto')
/** /**
* @typedef {Object} Config * @typedef {Object} Config
@ -107,6 +108,10 @@ const defaultConfig = {
hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'], hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'],
compress: ['none', 'zlib@openssh.com', 'zlib'] compress: ['none', 'zlib@openssh.com', 'zlib']
}, },
session: {
secret: generateSecureSecret(),
name: 'webssh2.sid'
},
serverlog: { serverlog: {
client: false, client: false,
server: false server: false
@ -208,6 +213,14 @@ const configSchema = {
}, },
required: ['kex', 'cipher', 'hmac', 'compress'] required: ['kex', 'cipher', 'hmac', 'compress']
}, },
session: {
type: 'object',
properties: {
secret: { type: 'string' },
name: { type: 'string' }
},
required: ['secret', 'name']
},
serverlog: { serverlog: {
type: 'object', type: 'object',
properties: { properties: {
@ -271,19 +284,25 @@ function logError(message, error) {
} }
/** /**
* Loads the configuration * Loads and merges the configuration
* @returns {Config} The loaded configuration * @returns {Config} The merged configuration
*/ */
function loadConfig() { function loadConfig() {
const configPath = getConfigPath() const configPath = getConfigPath()
try { try {
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
const config = readConfigFile(configPath) const providedConfig = readConfigFile(configPath)
return validateConfig(config)
// Deep merge the provided config with the default config
const mergedConfig = deepMerge(JSON.parse(JSON.stringify(defaultConfig)), providedConfig)
const validatedConfig = validateConfig(mergedConfig)
console.log('Merged and validated configuration')
return validatedConfig
} else { } else {
logError( logError(
'\n\nERROR: Missing config.json for webssh. Current config: ' + '\n\nERROR: Missing config.json for webssh. Using default config: ' +
JSON.stringify(defaultConfig) + JSON.stringify(defaultConfig) +
'\n\n See config.json.sample for details\n\n' '\n\n See config.json.sample for details\n\n'
) )
@ -291,7 +310,7 @@ function loadConfig() {
} }
} catch (err) { } catch (err) {
logError( logError(
'\n\nERROR: Missing config.json for webssh. Current config: ' + '\n\nERROR: Problem loading config.json for webssh. Using default config: ' +
JSON.stringify(defaultConfig) + JSON.stringify(defaultConfig) +
'\n\n See config.json.sample for details\n\n', '\n\n See config.json.sample for details\n\n',
err err
@ -300,6 +319,32 @@ function loadConfig() {
} }
} }
/**
* Generates a secure random session secret
* @returns {string} A random 32-byte hex string
*/
function generateSecureSecret() {
return crypto.randomBytes(32).toString('hex')
}
/**
* Deep merges two objects
* @param {Object} target - The target object to merge into
* @param {Object} source - The source object to merge from
* @returns {Object} The merged object
*/
function deepMerge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (source[key] instanceof Object && !Array.isArray(source[key])) {
target[key] = deepMerge(target[key] || {}, source[key])
} else {
target[key] = source[key]
}
}
}
return target
}
/** /**
* The loaded configuration * The loaded configuration
* @type {Config} * @type {Config}

View file

@ -17,18 +17,16 @@ function handleConnection(req, res, urlParams) {
// Inject configuration // Inject configuration
var config = { var config = {
socket: { socket: {
url: req.protocol + '://' + req.get('host'), url: req.protocol + '://' + req.get('host'),
path: '/ssh/socket.io' path: '/ssh/socket.io'
}, },
ssh: { ssh: {
host: connectionParams.host || '', host: urlParams.host || '',
port: connectionParams.port || 22, port: urlParams.port || 22
username: connectionParams.username || '', },
password: connectionParams.password || '' autoConnect: !!req.session.sshCredentials
}, };
autoConnect: !!(connectionParams.host && connectionParams.username && connectionParams.password)
};
// Read the client.htm file // Read the client.htm file
fs.readFile(path.join(clientPath, 'client.htm'), 'utf8', function(err, data) { fs.readFile(path.join(clientPath, 'client.htm'), 'utf8', function(err, data) {

View file

@ -1,9 +1,9 @@
// server // server
// /app/routes.js // /app/routes.js
var express = require('express'); const express = require('express');
var router = express.Router(); const router = express.Router();
var handleConnection = require('./connectionHandler'); const handleConnection = require('./connectionHandler');
var basicAuth = require('basic-auth'); const basicAuth = require('basic-auth');
function auth(req, res, next) { function auth(req, res, next) {
var credentials = basicAuth(req); var credentials = basicAuth(req);
@ -11,7 +11,8 @@ function auth(req, res, next) {
res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH2"'); res.setHeader('WWW-Authenticate', 'Basic realm="WebSSH2"');
return res.status(401).send('Authentication required.'); return res.status(401).send('Authentication required.');
} }
req.sshCredentials = credentials; // Store credentials in session
req.session.sshCredentials = credentials;
next(); next();
} }
@ -22,11 +23,7 @@ router.get('/', function(req, res) {
// Scenario 2: Auth required // Scenario 2: Auth required
router.get('/host/:host', auth, function(req, res) { router.get('/host/:host', auth, function(req, res) {
handleConnection(req, res, { handleConnection(req, res, { host: req.params.host });
host: req.params.host,
username: req.sshCredentials.name,
password: req.sshCredentials.pass
});
}); });
module.exports = router; module.exports = router;

View file

@ -59,6 +59,13 @@ function handleConnection(socket, config) {
*/ */
function handleAuthentication(socket, creds, config) { function handleAuthentication(socket, creds, config) {
console.log(`SOCKET AUTHENTICATE: ${socket.id}`) console.log(`SOCKET AUTHENTICATE: ${socket.id}`)
const sessionCreds = socket.handshake.session.sshCredentials;
if (sessionCreds) {
creds.username = sessionCreds.name;
creds.password = sessionCreds.pass;
}
if (isValidCredentials(creds)) { if (isValidCredentials(creds)) {
console.log(`SOCKET CREDENTIALS VALID: ${socket.id}`) console.log(`SOCKET CREDENTIALS VALID: ${socket.id}`)
initializeConnection(socket, creds, config) initializeConnection(socket, creds, config)

136
package-lock.json generated
View file

@ -325,6 +325,11 @@
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g==" "integrity": "sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g=="
}, },
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"base64id": { "base64id": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
@ -466,6 +471,15 @@
} }
} }
}, },
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"buffer-from": { "buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -748,6 +762,22 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==" "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="
}, },
"cookie-parser": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz",
"integrity": "sha512-YN/8nzPcK5o6Op4MIzAd4H4qUal5+3UaMhVIeaafFYL0pKvBQA/9Yhzo7ZwvBpjdGshsiTAb1+FC37M6RdPDFg==",
"requires": {
"cookie": "0.1.3",
"cookie-signature": "1.0.6"
},
"dependencies": {
"cookie": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz",
"integrity": "sha512-mWkFhcL+HVG1KjeCjEBVJJ7s4sAGMLiBDFSDs4bzzvgLZt7rW8BhP6XV/8b1+pNvx/skd3yYxPuaF3Z6LlQzyw=="
}
}
},
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -765,6 +795,14 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true "dev": true
}, },
"crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"requires": {
"buffer": "^5.1.0"
}
},
"create-error-class": { "create-error-class": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
@ -1617,6 +1655,81 @@
} }
} }
}, },
"express-session": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
"requires": {
"cookie": "0.6.0",
"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": {
"cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
},
"cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"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"
}
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"express-socket.io-session": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/express-socket.io-session/-/express-socket.io-session-1.3.5.tgz",
"integrity": "sha512-ila9jN7Pu9OuNIDzkuW+ZChR2Y0TzyyFITT7xiOWCjuGCDUWioD382zqxI7HOaa8kIhfs3wTLOZMU9h6buuOFw==",
"requires": {
"cookie-parser": "~1.3.3",
"crc": "^3.3.0",
"debug": "~2.6.0"
},
"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=="
}
}
},
"extend-shallow": { "extend-shallow": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -2182,6 +2295,11 @@
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
} }
}, },
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": { "ignore": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@ -3079,6 +3197,11 @@
"ee-first": "1.1.1" "ee-first": "1.1.1"
} }
}, },
"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": { "once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -3400,6 +3523,11 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true "dev": true
}, },
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
},
"range-parser": { "range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -4546,6 +4674,14 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"dev": true "dev": true
}, },
"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"
}
},
"unbox-primitive": { "unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View file

@ -37,6 +37,8 @@
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
"debug": "~4.1.0", "debug": "~4.1.0",
"express": "^4.14.1", "express": "^4.14.1",
"express-session": "^1.18.0",
"express-socket.io-session": "^1.3.5",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"read-config-ng": "~3.0.7", "read-config-ng": "~3.0.7",
"socket.io": "~2.2.0", "socket.io": "~2.2.0",