Files
proxy/nodejs/test/unit/basicauth.test.js
T
wmantly 653c79f099 Per-host SSO: data model + normalization (#57)
Add Host fields sso_enabled / sso_allow_users / sso_allow_groups (empty
allow-lists = any authenticated user) and normalize them (parseAllowList). SSO
reuses conf.oidc and is OR'd with basic auth. Enforcement (session cookie + Lua
+ nginx auth location) lands separately. Unit tests included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 12:01:54 -04:00

101 lines
3.5 KiB
JavaScript

'use strict';
const {describe, test} = require('node:test');
const assert = require('node:assert');
const {hashPassword, hashBasicAuthUsers} = require('../../utils/basicauth');
const {
parseBasicAuthLines,
sanitizeBasicAuthObject,
sanitizeRealm,
parseAllowList,
normalizeHostFeatures,
} = require('../../utils/host_features');
/**
* Per-host basic auth (#57). The hash must match what OpenResty computes in
* ops/nginx_conf/hostfeatures.lua: base64(sha1(password)) (htpasswd "{SHA}").
*/
describe('basicauth hashing', () => {
test('base64(sha1(password)) matches the known htpasswd {SHA} vector', () => {
assert.strictEqual(hashPassword('secret'), '5en6G6MezRroT3XKqkdPOmY/BfQ=');
});
test('hashBasicAuthUsers hashes each password, skips empties', () => {
assert.deepStrictEqual(
hashBasicAuthUsers({alice: 'secret', bob: '', carol: null}),
{alice: '5en6G6MezRroT3XKqkdPOmY/BfQ='}
);
});
});
describe('parseBasicAuthLines', () => {
test('parses user:password lines; passwords may contain colons', () => {
assert.deepStrictEqual(
parseBasicAuthLines('alice:secret\nbob:pw:with:colons'),
{alice: 'secret', bob: 'pw:with:colons'}
);
});
test('drops blank lines, lines without a colon, and empty passwords', () => {
assert.deepStrictEqual(
parseBasicAuthLines('\nalice:secret\nnopassword\nbob:\n \n'),
{alice: 'secret'}
);
});
test('rejects usernames with spaces/control chars', () => {
assert.deepStrictEqual(parseBasicAuthLines('a b:secret'), {});
});
});
describe('sanitizeRealm', () => {
test('strips CR/LF and quotes and trims', () => {
assert.strictEqual(sanitizeRealm('My "Realm"\r\n'), 'My Realm');
assert.strictEqual(sanitizeRealm(undefined), '');
});
});
describe('normalizeHostFeatures (basic auth)', () => {
test('coerces enabled, parses users to plaintext object, sanitizes realm', () => {
let body = {
basicauth_enabled: 'true',
basicauth_realm: 'Admins\n',
basicauth_users: 'alice:secret\nbob:pw',
};
normalizeHostFeatures(body);
assert.strictEqual(body.basicauth_enabled, true);
assert.strictEqual(body.basicauth_realm, 'Admins');
assert.deepStrictEqual(body.basicauth_users, {alice: 'secret', bob: 'pw'});
});
test('empty users input is dropped so a blank edit keeps existing users', () => {
let body = {basicauth_enabled: 'true', basicauth_users: ' \n'};
normalizeHostFeatures(body);
assert.ok(!('basicauth_users' in body));
});
test('object input is sanitized like text input', () => {
let body = {basicauth_users: {alice: 'secret', 'bad user': 'x', bob: ''}};
normalizeHostFeatures(body);
assert.deepStrictEqual(body.basicauth_users, {alice: 'secret'});
});
});
describe('SSO allow-lists (#57)', () => {
test('parseAllowList splits on commas/whitespace/newlines and dedupes', () => {
assert.deepStrictEqual(
parseAllowList('alice@x.com, bob@x.com\ncarol@x.com alice@x.com'),
['alice@x.com', 'bob@x.com', 'carol@x.com']
);
assert.deepStrictEqual(parseAllowList(['a', 'a', ' b ', '']), ['a', 'b']);
assert.deepStrictEqual(parseAllowList(''), []);
});
test('normalizeHostFeatures coerces sso_enabled and parses allow-lists', () => {
let body = {
sso_enabled: 'true',
sso_allow_users: 'alice@x.com\nbob@x.com',
sso_allow_groups: 'dns-team, admins',
};
normalizeHostFeatures(body);
assert.strictEqual(body.sso_enabled, true);
assert.deepStrictEqual(body.sso_allow_users, ['alice@x.com', 'bob@x.com']);
assert.deepStrictEqual(body.sso_allow_groups, ['dns-team', 'admins']);
});
});