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 path = require('path')
const bodyParser = require('body-parser')
const session = require('express-session')
const sharedsession = require("express-socket.io-session")
const config = require('./config')
const socketHandler = require('./socket')
const sshRoutes = require('./routes')
@ -16,10 +18,19 @@ const sshRoutes = require('./routes')
* @returns {express.Application} The Express application instance
*/
function createApp() {
var app = express();
const app = express();
// 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
app.use(bodyParser.urlencoded({ extended: true }));
@ -31,7 +42,7 @@ function createApp() {
// Use the SSH routes
app.use('/ssh', sshRoutes);
return app;
return { app, sessionMiddleware };
}
/**
@ -46,13 +57,21 @@ function createServer(app) {
/**
* Configures Socket.IO with the given server
* @param {http.Server} server - The HTTP server instance
* @param {Function} sessionMiddleware - The session middleware
* @returns {import('socket.io').Server} The Socket.IO server instance
*/
function configureSocketIO(server) {
return socketIo(server, {
function configureSocketIO(server, sessionMiddleware) {
const io = socketIo(server, {
path: '/ssh/socket.io',
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
*/
function startServer() {
const app = createApp()
const { app, sessionMiddleware } = createApp()
const server = createServer(app)
const io = configureSocketIO(server)
const io = configureSocketIO(server, sessionMiddleware)
// Set up Socket.IO listeners
setupSocketIOListeners(io)

View file

@ -6,6 +6,7 @@ const path = require('path')
const fs = require('fs')
const readConfig = require('read-config-ng')
const Ajv = require('ajv')
const crypto = require('crypto')
/**
* @typedef {Object} Config
@ -107,6 +108,10 @@ const defaultConfig = {
hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'],
compress: ['none', 'zlib@openssh.com', 'zlib']
},
session: {
secret: generateSecureSecret(),
name: 'webssh2.sid'
},
serverlog: {
client: false,
server: false
@ -208,6 +213,14 @@ const configSchema = {
},
required: ['kex', 'cipher', 'hmac', 'compress']
},
session: {
type: 'object',
properties: {
secret: { type: 'string' },
name: { type: 'string' }
},
required: ['secret', 'name']
},
serverlog: {
type: 'object',
properties: {
@ -271,19 +284,25 @@ function logError(message, error) {
}
/**
* Loads the configuration
* @returns {Config} The loaded configuration
* Loads and merges the configuration
* @returns {Config} The merged configuration
*/
function loadConfig() {
const configPath = getConfigPath()
try {
if (fs.existsSync(configPath)) {
const config = readConfigFile(configPath)
return validateConfig(config)
const providedConfig = readConfigFile(configPath)
// 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 {
logError(
'\n\nERROR: Missing config.json for webssh. Current config: ' +
'\n\nERROR: Missing config.json for webssh. Using default config: ' +
JSON.stringify(defaultConfig) +
'\n\n See config.json.sample for details\n\n'
)
@ -291,7 +310,7 @@ function loadConfig() {
}
} catch (err) {
logError(
'\n\nERROR: Missing config.json for webssh. Current config: ' +
'\n\nERROR: Problem loading config.json for webssh. Using default config: ' +
JSON.stringify(defaultConfig) +
'\n\n See config.json.sample for details\n\n',
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
* @type {Config}

View file

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

View file

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

View file

@ -59,6 +59,13 @@ function handleConnection(socket, config) {
*/
function handleAuthentication(socket, creds, config) {
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)) {
console.log(`SOCKET CREDENTIALS VALID: ${socket.id}`)
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",
"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": {
"version": "1.0.0",
"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": {
"version": "1.1.2",
"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",
"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": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -765,6 +795,14 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"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": {
"version": "3.0.2",
"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": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -2182,6 +2295,11 @@
"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": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@ -3079,6 +3197,11 @@
"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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -3400,6 +3523,11 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -4546,6 +4674,14 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View file

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