Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions lib/helpers/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,25 @@ const validateDomainIsSet = (config) => {
* Known default / placeholder JWT secret values that must never be used in
* non-dev environments. Extend this list when a new downstream project is
* bootstrapped with its own placeholder.
* @readonly
*/
const JWT_DEFAULT_SECRETS = new Set([
const JWT_DEFAULT_SECRETS = Object.freeze(new Set([
'WaosSecretKeyExampleToChnageAbsolutely', // devkit upstream placeholder
'TrawlNodeDevSecret', // trawl downstream placeholder
Comment on lines +61 to 65
'ComesNodeDevSecret', // comes downstream placeholder
'MontaineNodeDevSecret', // montaine downstream placeholder
'PierrebNodeDevSecret', // pierreb downstream placeholder
'IsmNodeDevSecret', // ism downstream placeholder
]);
]));

/**
* @desc Canonical JWT weakness predicate — single source of truth.
* Returns true when the secret is empty, whitespace-only, shorter than 32
* characters, or matches a known default placeholder.
* @param {string|null|undefined} secret - raw JWT secret value
* @returns {boolean}
*/
const isJwtSecretWeak = (secret) => !secret || secret.trim() === '' || secret.length < 32 || JWT_DEFAULT_SECRETS.has(secret);

/**
* Safe envs where a weak / default secret is tolerated (warn only).
Expand All @@ -86,7 +96,7 @@ const validateJwtSecret = (config) => {
const secret = config.jwt?.secret;
const env = process.env.NODE_ENV ?? 'development';

const isWeak = !secret || secret.trim() === '' || secret.length < 32 || JWT_DEFAULT_SECRETS.has(secret);
const isWeak = isJwtSecretWeak(secret);

if (!isWeak) return; // strong secret — nothing to do

Expand Down Expand Up @@ -201,6 +211,8 @@ const initGlobalConfigFiles = async (assets) => {
export default {
getGlobbedPaths,
validateDomainIsSet,
JWT_DEFAULT_SECRETS,
isJwtSecretWeak,
validateJwtSecret,
initSecureMode,
initGlobalConfigFiles,
Expand Down
94 changes: 94 additions & 0 deletions lib/helpers/tests/config.isJwtSecretWeak.unit.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Unit tests for isJwtSecretWeak in config helper.
*
* Verifies that the exported helper returns the same verdicts as the two
* former private predicates it replaces (validateJwtSecret + jwtInsecure in
* home.service.js). Zero behavior change is the acceptance criterion.
*
* Weak cases (returns true):
* - undefined / null / empty string / whitespace-only
* - length < 32 characters
* - each known default placeholder (devkit + all downstream)
*
* Strong case (returns false):
* - a ≥ 32-char string that is not in the defaults list
*/
import { describe, test, expect } from '@jest/globals';
import configHelper from '../config.js';

const { isJwtSecretWeak, JWT_DEFAULT_SECRETS } = configHelper;

const STRONG_SECRET = 'a'.repeat(32); // exactly 32 chars, non-default

describe('config.isJwtSecretWeak', () => {
// ---- weak: empty / missing ------------------------------------------------

test('undefined → true (weak)', () => {
expect(isJwtSecretWeak(undefined)).toBe(true);
});

test('null → true (weak)', () => {
expect(isJwtSecretWeak(null)).toBe(true);
});

test('empty string → true (weak)', () => {
expect(isJwtSecretWeak('')).toBe(true);
});

test('whitespace-only string → true (weak)', () => {
expect(isJwtSecretWeak(' ')).toBe(true);
});

// ---- weak: too short ------------------------------------------------------

test('short secret (< 32 chars, not a known default) → true (weak)', () => {
expect(isJwtSecretWeak('tooshort')).toBe(true);
});

test('31-char secret → true (weak)', () => {
expect(isJwtSecretWeak('a'.repeat(31))).toBe(true);
});

// ---- weak: each known default placeholder ---------------------------------

test('devkit placeholder → true (weak)', () => {
expect(isJwtSecretWeak('WaosSecretKeyExampleToChnageAbsolutely')).toBe(true);
});

test('TrawlNodeDevSecret → true (weak)', () => {
expect(isJwtSecretWeak('TrawlNodeDevSecret')).toBe(true);
});

test('ComesNodeDevSecret → true (weak)', () => {
expect(isJwtSecretWeak('ComesNodeDevSecret')).toBe(true);
});

test('MontaineNodeDevSecret → true (weak)', () => {
expect(isJwtSecretWeak('MontaineNodeDevSecret')).toBe(true);
});

test('PierrebNodeDevSecret → true (weak)', () => {
expect(isJwtSecretWeak('PierrebNodeDevSecret')).toBe(true);
});

test('IsmNodeDevSecret → true (weak)', () => {
expect(isJwtSecretWeak('IsmNodeDevSecret')).toBe(true);
});

// covers the full JWT_DEFAULT_SECRETS set exhaustively
test('all entries in JWT_DEFAULT_SECRETS → true (weak)', () => {
for (const s of JWT_DEFAULT_SECRETS) {
expect(isJwtSecretWeak(s)).toBe(true);
}
});

// ---- strong ---------------------------------------------------------------

test('32-char non-default secret → false (strong)', () => {
expect(isJwtSecretWeak(STRONG_SECRET)).toBe(false);
});

test('long strong secret → false (strong)', () => {
expect(isJwtSecretWeak('supersecret_random_string_at_least_32_chars!')).toBe(false);
});
});
18 changes: 5 additions & 13 deletions modules/home/services/home.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { promises as fs } from 'fs';
import mongoose from 'mongoose';

import config from '../../../config/index.js';
import configHelper from '../../../lib/helpers/config.js';
import mailer from '../../../lib/helpers/mailer/index.js';
import HomeRepository from '../repositories/home.repository.js';

const { isJwtSecretWeak } = configHelper;

/**
* @desc Check whether a config value is meaningfully set (non-empty, not a DEVKIT placeholder).
* @param {*} value - Config value to check
Expand Down Expand Up @@ -73,19 +76,8 @@ const getReadinessStatus = () => {
message: domainSet ? 'Domain configured' : 'Domain not configured',
});

// security — JWT secret
// Re-use the same weakness predicate as validateJwtSecret (config helper):
// empty / whitespace / < 32 chars / known default placeholder.
const JWT_DEFAULTS = new Set([
'WaosSecretKeyExampleToChnageAbsolutely',
'TrawlNodeDevSecret',
'ComesNodeDevSecret',
'MontaineNodeDevSecret',
'PierrebNodeDevSecret',
'IsmNodeDevSecret',
]);
const jwtSecret = config.jwt?.secret;
const jwtInsecure = !jwtSecret || jwtSecret.trim() === '' || jwtSecret.length < 32 || JWT_DEFAULTS.has(jwtSecret);
// security — JWT secret (uses shared isJwtSecretWeak from config helper)
const jwtInsecure = isJwtSecretWeak(config.jwt?.secret);
checks.push({
category: 'security',
status: jwtInsecure ? 'warning' : 'ok',
Expand Down
25 changes: 25 additions & 0 deletions modules/home/tests/home.service.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,29 @@ describe('HomeService.getReadinessStatus unit tests:', () => {
expect(row.status).toBe('warning');
});
});

describe('security (JWT) row — uses shared isJwtSecretWeak', () => {
test('warning when jwt.secret is a known default placeholder', async () => {
const HomeService = await withConfig({ jwt: { secret: 'WaosSecretKeyExampleToChnageAbsolutely' } });
const checks = HomeService.getReadinessStatus();
const row = checks.find((c) => c.category === 'security');
expect(row.status).toBe('warning');
expect(row.message).toContain('known default');
});

test('warning when jwt.secret is too short', async () => {
const HomeService = await withConfig({ jwt: { secret: 'tooshort' } });
const checks = HomeService.getReadinessStatus();
const row = checks.find((c) => c.category === 'security');
expect(row.status).toBe('warning');
});

test('ok when jwt.secret is a strong (≥32-char, non-default) secret', async () => {
const HomeService = await withConfig({ jwt: { secret: 'a'.repeat(32) } });
const checks = HomeService.getReadinessStatus();
const row = checks.find((c) => c.category === 'security');
expect(row.status).toBe('ok');
expect(row.message).toBe('JWT secret is custom');
});
});
});
Loading