mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-18 13:08:39 +02:00
Harden login request validation
This commit is contained in:
parent
e7af9d518c
commit
706c6447eb
4 changed files with 103 additions and 4 deletions
85
test/backend/authenticate.test.mjs
Normal file
85
test/backend/authenticate.test.mjs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import * as otplib from 'otplib';
|
||||
import { authenticateUser } from '../../backend/controllers/shared/authenticate.js';
|
||||
import { Common } from '../../backend/utils/common.js';
|
||||
|
||||
const { authenticator } = otplib;
|
||||
|
||||
const TOTP_SECRET = 'JBSWY3DPEHPK3PXP';
|
||||
const PASSWORD_HASH = 'hashed-password';
|
||||
|
||||
const setupAppConfig = (secret2FA) => {
|
||||
Common.appConfig = {
|
||||
defaultNodeIndex: 0,
|
||||
selectedNodeIndex: 0,
|
||||
rtlConfFilePath: '',
|
||||
dbDirectoryPath: '',
|
||||
rtlPass: PASSWORD_HASH,
|
||||
allowPasswordUpdate: true,
|
||||
enable2FA: !!secret2FA,
|
||||
secret2FA: secret2FA,
|
||||
SSO: { rtlSSO: 0, rtlCookiePath: '', logoutRedirectLink: '', cookieValue: '' },
|
||||
nodes: []
|
||||
};
|
||||
Common.selectedNode = null;
|
||||
Common.nodes = [];
|
||||
};
|
||||
|
||||
// Distinct IPs per call: failedLoginAttempts is module-level state in authenticate.js,
|
||||
// keyed by request IP, so unique values keep the tests isolated from each other.
|
||||
let ipCounter = 0;
|
||||
const mockRequest = (twoFAToken) => {
|
||||
ipCounter = ipCounter + 1;
|
||||
return {
|
||||
body: { authenticateWith: 'PASSWORD', authenticationValue: PASSWORD_HASH, twoFAToken: twoFAToken },
|
||||
session: {},
|
||||
headers: { 'x-forwarded-for': '10.0.0.' + ipCounter },
|
||||
connection: {},
|
||||
socket: {}
|
||||
};
|
||||
};
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = { statusCode: null, body: null };
|
||||
res.status = (code) => {
|
||||
res.statusCode = code;
|
||||
return { json: (body) => { res.body = body; } };
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
test('rejects login without a 2FA token when 2FA is enabled', () => {
|
||||
setupAppConfig(TOTP_SECRET);
|
||||
for (const missingToken of [undefined, '']) {
|
||||
const res = mockResponse();
|
||||
authenticateUser(mockRequest(missingToken), res, null);
|
||||
assert.equal(res.statusCode, 401);
|
||||
assert.match(res.body.error, /2FA/);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects login with an invalid 2FA token when 2FA is enabled', () => {
|
||||
setupAppConfig(TOTP_SECRET);
|
||||
const res = mockResponse();
|
||||
authenticateUser(mockRequest('000000'), res, null);
|
||||
assert.equal(res.statusCode, 401);
|
||||
assert.match(res.body.error, /2FA/);
|
||||
});
|
||||
|
||||
test('accepts login with a valid 2FA token when 2FA is enabled', () => {
|
||||
setupAppConfig(TOTP_SECRET);
|
||||
const res = mockResponse();
|
||||
authenticateUser(mockRequest(authenticator.generate(TOTP_SECRET)), res, null);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(typeof res.body.token, 'string');
|
||||
});
|
||||
|
||||
test('accepts password-only login when 2FA is not configured', () => {
|
||||
setupAppConfig('');
|
||||
const res = mockResponse();
|
||||
authenticateUser(mockRequest(undefined), res, null);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(typeof res.body.token, 'string');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue