RTL/backend/utils/authCheck.js
saubyk e617fbb561 Replace deprecated csurf with csrf-csrf
csurf has been deprecated since 2022 and pins an old cookie release
with a known advisory; npm's only fix is a downgrade (issue #1634,
item 2). csrf-csrf v4 implements the same double-submit-cookie pattern
with an HMAC-signed, session-bound token keyed on the existing boot
secret (common.secret_key).

The frontend contract is unchanged: the token still arrives via the
XSRF-TOKEN cookie/header and is echoed as x-xsrf-token (all token
sources csurf accepted are still read), the signed cookie keeps the
_csrf name (now httpOnly, secure:false to match the session cookie on
plain-HTTP deployments), doubleCsrfProtection attaches req.csrfToken
so app.ts keeps working, and the error code is EBADCSRFTOKEN - already
handled in app.ts. The websocket upgrade check in authCheck.ts now
routes through the shared middleware; upgrade requests are GETs, so
its pass-through semantics are unchanged.

One fix this surfaced: app.ts called req.csrfToken() twice (cookie and
header). Under csurf every token validated against a stable secret;
under csrf-csrf each first-visit call mints a new token, desyncing the
XSRF-TOKEN cookie from the _csrf cookie it must equal. The token is
now generated once per request.

Tokens are session-bound, so a token stolen from one session no longer
validates in another - a check csurf's cookie mode did not perform.

Production npm audit drops from 6 low findings to 4, all in the
crypto-browserify/elliptic chain tracked in #1634.

Verified against the docker regtest fixture: both API suites (43
checks across LND, CLN and Eclair) plus a dedicated CSRF battery -
valid-token auth, missing token 403, garbage token 403, cross-session
replay 403, token stability across requests, the XSRF-TOKEN response
header for Quickpay, and the websocket handshake. Lint and build are
clean.
2026-07-19 22:01:23 -07:00

67 lines
2.9 KiB
JavaScript

import jwt from 'jsonwebtoken';
import CSRF from './csrf.js';
import { Common } from './common.js';
import { Logger } from './logger.js';
const common = Common;
const logger = Logger;
const csurfProtection = CSRF.csrfProtection;
export const isAuthenticated = (req, res, next) => {
try {
const token = req.headers.authorization.split(' ')[1];
jwt.verify(token, common.secret_key);
next();
}
catch (error) {
const errMsg = 'Authentication Failed! Please Login First!';
const err = common.handleError({ statusCode: 401, message: 'Authentication Error', error: errMsg }, 'AuthCheck', errMsg, req.session.selectedNode);
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
};
export const verifyWSUser = (info, next) => {
const headers = JSON.parse(JSON.stringify(info.req.headers));
const protocols = !info.req.headers['sec-websocket-protocol'] ? [] : info.req.headers['sec-websocket-protocol'].split(',')?.map((s) => s.trim());
const jwToken = (protocols && protocols.length > 0) ? protocols[0] : '';
if (!jwToken || jwToken === '') {
next(false, 401, 'Authentication Failed! Please Login First!');
}
else {
jwt.verify(jwToken, common.secret_key, (verificationErr) => {
if (verificationErr) {
next(false, 401, 'Authentication Failed! Please Login First!');
}
else {
try {
let updatedReq = null;
try {
updatedReq = JSON.parse(JSON.stringify(info.req));
}
catch (err) {
updatedReq = info.req;
}
let cookies = null;
try {
cookies = '{"' + headers.cookie?.replace(/ /g, '')?.replace(/;/g, '","').trim()?.replace(/[=]/g, '":"') + '"}';
updatedReq['cookies'] = JSON.parse(cookies);
}
catch (err) {
cookies = {};
updatedReq['cookies'] = JSON.parse(cookies);
logger.log({ selectedNode: common.selectedNode, level: 'WARN', fileName: 'AuthCheck', msg: '403 Unable to read CSRF token cookie', data: err });
}
csurfProtection(updatedReq, null, (err) => {
if (err) {
next(false, 403, 'Invalid CSRF token!');
}
else {
next(true);
}
});
}
catch (err) {
logger.log({ selectedNode: common.selectedNode, level: 'WARN', fileName: 'AuthCheck', msg: '403 Unable to verify CSRF token', data: err });
next(true);
}
}
});
}
};