mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:37:08 +00:00
Fix authentication middleware erroring on IPv6
This commit is contained in:
@@ -1,41 +1,122 @@
|
|||||||
const dns = require('dns');
|
const dns = require('dns');
|
||||||
const { Netmask } = require('netmask');
|
const ipaddr = require('ipaddr.js');
|
||||||
|
const { isIPv6, isIPv4 } = require('net');
|
||||||
const cfg = require('../../../lib/config');
|
const cfg = require('../../../lib/config');
|
||||||
const jwt = require('../../../lib/jwt');
|
const jwt = require('../../../lib/jwt');
|
||||||
const user = require('../../../lib/db/user');
|
const user = require('../../../lib/db/user');
|
||||||
const ServerUser = require('../../../lib/db/user/User');
|
const ServerUser = require('../../../lib/db/user/User');
|
||||||
|
|
||||||
async function authorisedIP (req, res) {
|
function parseIP(ip) {
|
||||||
const validIPs = await user.ip({active: true}); // Get all active IP logins
|
if (!ip || typeof ip !== 'string') {
|
||||||
validIPs.forEach( i => i.$block = new Netmask(i.ip) );
|
console.warn('Invalid IP input:', ip);
|
||||||
validIPs.sort( (a, b) => b.$block.bitmask - a.$block.netmask ); // More specific IPs have precedence
|
return null;
|
||||||
for (const ip of validIPs) {
|
}
|
||||||
const block = ip.$block;
|
// Handle comma-separated X-Forwarded-For (e.g., "87.90.254.127,")
|
||||||
if (block.contains(req.ip)) {
|
const cleanIp = ip.split(',')[0].trim();
|
||||||
const payload = {
|
if (!cleanIp) {
|
||||||
...ip,
|
console.warn('Empty IP after parsing:', ip);
|
||||||
ip: req.ip,
|
return null;
|
||||||
autologin: true
|
}
|
||||||
};
|
// Convert IPv6-mapped IPv4 (e.g., ::ffff:127.0.0.1 -> 127.0.0.1)
|
||||||
delete payload.$block;
|
if (cleanIp.startsWith('::ffff:') && isIPv4(cleanIp.split('::ffff:')[1])) {
|
||||||
delete payload.hash;
|
return cleanIp.split('::ffff:')[1];
|
||||||
delete payload.active;
|
}
|
||||||
jwt.issue(payload, req, res);
|
return cleanIp;
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
function normalizeCIDR(range) {
|
||||||
|
if (!range || typeof range !== 'string') {
|
||||||
|
console.warn('Invalid CIDR range:', range);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// If no /prefix, assume /32 for IPv4 or /128 for IPv6
|
||||||
|
if (!range.includes('/')) {
|
||||||
|
try {
|
||||||
|
const parsed = ipaddr.parse(range);
|
||||||
|
const prefix = parsed.kind() === 'ipv4' ? 32 : 128;
|
||||||
|
return `${range}/${prefix}`;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to parse bare IP ${range}:`, err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authorisedIP(req, res) {
|
||||||
|
const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']);
|
||||||
|
console.log('authorisedIP:', { ip, headers: req.headers }); // Debug
|
||||||
|
if (!ip) {
|
||||||
|
console.warn('No valid IP provided:', { ip, headers: req.headers });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let addr;
|
||||||
|
try {
|
||||||
|
addr = ipaddr.parse(ip);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Invalid IP:', ip, err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validIPs = await user.ip({ active: true }); // Get active IP logins
|
||||||
|
// Attach parsed CIDR to each IP entry
|
||||||
|
validIPs.forEach(i => {
|
||||||
|
const normalized = normalizeCIDR(i.ip);
|
||||||
|
if (!normalized) {
|
||||||
|
i.$range = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [rangeAddr, prefix] = ipaddr.parseCIDR(normalized);
|
||||||
|
i.$range = { addr: rangeAddr, prefix };
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Invalid CIDR range ${i.ip}:`, err.message);
|
||||||
|
i.$range = null; // Skip invalid ranges
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Filter out invalid ranges and sort by specificity (descending prefix length)
|
||||||
|
const validRanges = validIPs.filter(i => i.$range).sort((a, b) => b.$range.prefix - a.$range.prefix);
|
||||||
|
|
||||||
|
for (const ipEntry of validRanges) {
|
||||||
|
const { addr: rangeAddr, prefix } = ipEntry.$range;
|
||||||
|
try {
|
||||||
|
if (addr.match(rangeAddr, prefix)) {
|
||||||
|
const payload = {
|
||||||
|
...ipEntry,
|
||||||
|
ip,
|
||||||
|
autologin: true
|
||||||
|
};
|
||||||
|
delete payload.$range;
|
||||||
|
delete payload.hash;
|
||||||
|
delete payload.active;
|
||||||
|
jwt.issue(payload, req, res);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Error checking range ${ipEntry.ip}:`, err.message);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authorisedHost (req, res) {
|
async function authorisedHost(req, res) {
|
||||||
const validHosts = await user.host({active: true}); // Get all active host logins
|
const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']);
|
||||||
|
console.log('authorisedHost:', { ip, headers: req.headers }); // Debug
|
||||||
|
if (!ip) {
|
||||||
|
console.warn('No valid IP for host check:', { ip, headers: req.headers });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validHosts = await user.host({ active: true });
|
||||||
for (const key in validHosts) {
|
for (const key in validHosts) {
|
||||||
try {
|
try {
|
||||||
const ip = await dns.promises.resolve(key);
|
const resolvedIPs = await dns.promises.resolve(key);
|
||||||
if (ip == req.ip) {
|
if (resolvedIPs.includes(ip)) {
|
||||||
const payload = {
|
const payload = {
|
||||||
...validHosts[key],
|
...validHosts[key],
|
||||||
ip: req.ip,
|
ip,
|
||||||
autologin: true
|
autologin: true
|
||||||
};
|
};
|
||||||
delete payload.$block;
|
delete payload.$block;
|
||||||
@@ -45,49 +126,28 @@ async function authorisedHost (req, res) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code != "ENODATA") {
|
if (err.code !== 'ENODATA') {
|
||||||
console.error(err);
|
console.error(`DNS error for host ${key}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check client TLS certificates
|
async function auth(req, res, next) {
|
||||||
// Probably will do this via Nginx with
|
|
||||||
// ssl_verify_client optional;
|
|
||||||
// and then putting either of the
|
|
||||||
// $ssl_client_s_dn or $ssl_client_escaped_cert
|
|
||||||
// variables into an HTTP header for Node
|
|
||||||
// to check (naturally, it must be ensured
|
|
||||||
// that a user cannot just insert the header
|
|
||||||
// in a request).
|
|
||||||
|
|
||||||
|
|
||||||
async function auth (req, res, next) {
|
|
||||||
|
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
// Nothing to do, this request must have been
|
return; // Handled by another middleware
|
||||||
// handled already by another middleware.
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a valid JWT (already decoded by a previous
|
// Check for valid JWT
|
||||||
// middleware).
|
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
if (!req.user.autologin) {
|
if (!req.user.autologin && req.user.exp) {
|
||||||
// If this is not an automatic login, check if the token is in the
|
const ttl = req.user.exp - Date.now() / 1000;
|
||||||
// second half of its lifetime. If so, reissue a new one, valid for
|
if (ttl < cfg.jwt.options.expiresIn / 2) {
|
||||||
// another cfg.jwt.options.expiresIn seconds.
|
const credentials = await ServerUser.fromSQL(null, req.user.id);
|
||||||
if (req.user.exp) {
|
if (credentials) {
|
||||||
const ttl = req.user.exp - Date.now()/1000;
|
const payload = Object.assign({}, credentials.toJSON());
|
||||||
if (ttl < cfg.jwt.options.expiresIn/2) {
|
jwt.issue(payload, req, res);
|
||||||
const credentials = await ServerUser.fromSQL(null, req.user.id);
|
|
||||||
if (credentials) {
|
|
||||||
// Refresh token
|
|
||||||
payload = Object.assign({}, credentials.toJSON());
|
|
||||||
jwt.issue(Object.assign({}, credentials.toJSON()), req, res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,19 +155,17 @@ async function auth (req, res, next) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the IP is known to us
|
// Check IP and host
|
||||||
if (await authorisedIP(req, res)) {
|
if (await authorisedIP(req, res)) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the hostname is known to us
|
|
||||||
if (await authorisedHost(req, res)) {
|
if (await authorisedHost(req, res)) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
next({status: 401, message: "Not authorised"});
|
next({ status: 401, message: 'Not authorised' });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = auth;
|
module.exports = auth;
|
||||||
|
|||||||
@@ -37,11 +37,11 @@
|
|||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-jwt": "^8.4.1",
|
"express-jwt": "^8.4.1",
|
||||||
|
"ipaddr.js": "^1.9.1",
|
||||||
"json2csv": "^5.0.6",
|
"json2csv": "^5.0.6",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"leaflet-headless": "git+https://git@gitlab.com/aaltronav/contrib/leaflet-headless.git#devel",
|
"leaflet-headless": "git+https://git@gitlab.com/aaltronav/contrib/leaflet-headless.git#devel",
|
||||||
"marked": "^4.0.12",
|
"marked": "^4.0.12",
|
||||||
"netmask": "^2.0.2",
|
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nunjucks": "^3.2.3",
|
"nunjucks": "^3.2.3",
|
||||||
"path-to-regexp": "^6.2.1",
|
"path-to-regexp": "^6.2.1",
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -5366,14 +5366,6 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/www/client/source/node_modules/ipaddr.js": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lib/www/client/source/node_modules/is-arrayish": {
|
"lib/www/client/source/node_modules/is-arrayish": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9375,11 +9367,11 @@
|
|||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-jwt": "^8.4.1",
|
"express-jwt": "^8.4.1",
|
||||||
|
"ipaddr.js": "^1.9.1",
|
||||||
"json2csv": "^5.0.6",
|
"json2csv": "^5.0.6",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"leaflet-headless": "git+https://git@gitlab.com/aaltronav/contrib/leaflet-headless.git#devel",
|
"leaflet-headless": "git+https://git@gitlab.com/aaltronav/contrib/leaflet-headless.git#devel",
|
||||||
"marked": "^4.0.12",
|
"marked": "^4.0.12",
|
||||||
"netmask": "^2.0.2",
|
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nunjucks": "^3.2.3",
|
"nunjucks": "^3.2.3",
|
||||||
"path-to-regexp": "^6.2.1",
|
"path-to-regexp": "^6.2.1",
|
||||||
@@ -10180,13 +10172,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/www/server/node_modules/netmask": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lib/www/server/node_modules/nunjucks": {
|
"lib/www/server/node_modules/nunjucks": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
@@ -15585,6 +15570,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-buffer": {
|
"node_modules/is-buffer": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user