0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-23 22:27:34 -05:00
verdaccio/packages/plugins/htpasswd/tests/utils.test.ts
Ed Clement aeff267d94
fix: refactor htpasswd plugin to use the bcryptjs 'compare' api call instead of 'compareSync' (#3025)
feat: add a new configuration value named 'slow_verify_ms' to the htpasswd plugin that when exceeded during password verification will log a warning message
chore: update README.md for htpasswd plugin to add additional information about the 'rounds' configuration value and also include the new 'slow_verify_ms' configuration value
2022-03-03 21:57:19 +01:00

287 lines
10 KiB
TypeScript

// @ts-ignore: Module has no default export
import crypto from 'crypto';
import MockDate from 'mockdate';
import { DEFAULT_BCRYPT_ROUNDS } from '../src/htpasswd';
import {
HtpasswdHashAlgorithm,
addUserToHTPasswd,
changePasswordToHTPasswd,
generateHtpasswdLine,
lockAndRead,
parseHTPasswd,
sanityCheck,
verifyPassword,
} from '../src/utils';
const mockReadFile = jest.fn();
const mockUnlockFile = jest.fn();
const defaultHashConfig = {
algorithm: HtpasswdHashAlgorithm.bcrypt,
rounds: DEFAULT_BCRYPT_ROUNDS,
};
const mockTimeAndRandomBytes = () => {
MockDate.set('2018-01-14T11:17:40.712Z');
crypto.randomBytes = jest.fn(() => {
return {
toString: (): string => '$6',
};
});
Math.random = jest.fn(() => 0.38849);
};
jest.mock('@verdaccio/file-locking', () => ({
readFile: () => mockReadFile(),
unlockFile: () => mockUnlockFile(),
}));
describe('parseHTPasswd', () => {
it('should parse the password for a single line', () => {
const input = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z';
const output = { test: '$6b9MlB3WUELU' };
expect(parseHTPasswd(input)).toEqual(output);
});
it('should parse the password for two lines', () => {
const input = `user1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
user2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`;
const output = { user1: '$6b9MlB3WUELU', user2: '$6FrCaT/v0dwE' };
expect(parseHTPasswd(input)).toEqual(output);
});
it('should parse the password for multiple lines', () => {
const input = `user1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
user2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z
user3:$6FrCdfd\v0dwE:autocreated 2017-12-14T13:30:20.838Z
user4:$6FrCasdvppdwE:autocreated 2017-12-14T13:30:20.838Z`;
const output = {
user1: '$6b9MlB3WUELU',
user2: '$6FrCaT/v0dwE',
user3: '$6FrCdfd\v0dwE',
user4: '$6FrCasdvppdwE',
};
expect(parseHTPasswd(input)).toEqual(output);
});
});
describe('verifyPassword', () => {
it('should verify the MD5/Crypt3 password with true', async () => {
const input = ['test', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0'];
expect(await verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the MD5/Crypt3 password with false', async () => {
const input = ['testpasswordchanged', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0'];
expect(await verifyPassword(input[0], input[1])).toBeFalsy();
});
it('should verify the plain password with true', async () => {
const input = ['testpasswordchanged', '{PLAIN}testpasswordchanged'];
expect(await verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the plain password with false', async () => {
const input = ['testpassword', '{PLAIN}testpasswordchanged'];
expect(await verifyPassword(input[0], input[1])).toBeFalsy();
});
it('should verify the crypto SHA password with true', async () => {
const input = ['testpassword', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0='];
expect(await verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the crypto SHA password with false', async () => {
const input = ['testpasswordchanged', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0='];
expect(await verifyPassword(input[0], input[1])).toBeFalsy();
});
it('should verify the bcrypt password with true', async () => {
const input = ['testpassword', '$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO'];
expect(await verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the bcrypt password with false', async () => {
const input = [
'testpasswordchanged',
'$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO',
];
expect(await verifyPassword(input[0], input[1])).toBeFalsy();
});
});
describe('generateHtpasswdLine', () => {
beforeAll(mockTimeAndRandomBytes);
const [user, passwd] = ['username', 'password'];
it('should correctly generate line for md5', () => {
const md5Conf = { algorithm: HtpasswdHashAlgorithm.md5 };
expect(generateHtpasswdLine(user, passwd, md5Conf)).toMatchSnapshot();
});
it('should correctly generate line for sha1', () => {
const sha1Conf = { algorithm: HtpasswdHashAlgorithm.sha1 };
expect(generateHtpasswdLine(user, passwd, sha1Conf)).toMatchSnapshot();
});
it('should correctly generate line for crypt', () => {
const cryptConf = { algorithm: HtpasswdHashAlgorithm.crypt };
expect(generateHtpasswdLine(user, passwd, cryptConf)).toMatchSnapshot();
});
it('should correctly generate line for bcrypt', () => {
const bcryptAlgoConfig = {
algorithm: HtpasswdHashAlgorithm.bcrypt,
rounds: 2,
};
expect(generateHtpasswdLine(user, passwd, bcryptAlgoConfig)).toMatchSnapshot();
});
});
describe('addUserToHTPasswd - bcrypt', () => {
beforeAll(mockTimeAndRandomBytes);
it('should add new htpasswd to the end', () => {
const input = ['', 'username', 'password'];
expect(addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)).toMatchSnapshot();
});
it('should add new htpasswd to the end in multiline input', () => {
const body = `test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`;
const input = [body, 'username', 'password'];
expect(addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)).toMatchSnapshot();
});
it('should throw an error for incorrect username with space', () => {
const [a, b, c] = ['', 'firstname lastname', 'password'];
expect(() => addUserToHTPasswd(a, b, c, defaultHashConfig)).toThrowErrorMatchingSnapshot();
});
});
describe('lockAndRead', () => {
it('should call the readFile method', () => {
// console.log(fileLocking);
// const spy = jest.spyOn(fileLocking, 'readFile');
const cb = (): void => {};
lockAndRead('.htpasswd', cb);
expect(mockReadFile).toHaveBeenCalled();
});
});
describe('sanityCheck', () => {
let users;
beforeEach(() => {
users = { test: '$6FrCaT/v0dwE' };
});
test('should throw error for user already exists', async () => {
const verifyFn = jest.fn();
const input = await sanityCheck('test', users.test, verifyFn, users, Infinity);
expect(input.status).toEqual(401);
expect(input.message).toEqual('unauthorized access');
expect(verifyFn).toHaveBeenCalled();
});
test('should throw error for registration disabled of users', async () => {
const verifyFn = (): void => {};
const input = await sanityCheck('username', users.test, verifyFn, users, -1);
expect(input.status).toEqual(409);
expect(input.message).toEqual('user registration disabled');
});
test('should throw error max number of users', async () => {
const verifyFn = (): void => {};
const input = await sanityCheck('username', users.test, verifyFn, users, 1);
expect(input.status).toEqual(403);
expect(input.message).toEqual('maximum amount of users reached');
});
test('should not throw anything and sanity check', async () => {
const verifyFn = (): void => {};
const input = await sanityCheck('username', users.test, verifyFn, users, 2);
expect(input).toBeNull();
});
test('should throw error for required username field', async () => {
const verifyFn = (): void => {};
const input = await sanityCheck(undefined, users.test, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400);
});
test('should throw error for required password field', async () => {
const verifyFn = (): void => {};
const input = await sanityCheck('username', undefined, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400);
});
test('should throw error for required username & password fields', async () => {
const verifyFn = (): void => {};
const input = await sanityCheck(undefined, undefined, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400);
});
test('should throw error for existing username and password', async () => {
const verifyFn = jest.fn(() => true);
const input = await sanityCheck('test', users.test, verifyFn, users, 2);
expect(input.status).toEqual(409);
expect(input.message).toEqual('username is already registered');
expect(verifyFn).toHaveBeenCalledTimes(1);
});
test(
'should throw error for existing username and password with max number ' + 'of users reached',
async () => {
const verifyFn = jest.fn(() => true);
const input = await sanityCheck('test', users.test, verifyFn, users, 1);
expect(input.status).toEqual(409);
expect(input.message).toEqual('username is already registered');
expect(verifyFn).toHaveBeenCalledTimes(1);
}
);
});
describe('changePasswordToHTPasswd', () => {
test('should throw error for wrong password', async () => {
const body = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z';
try {
await changePasswordToHTPasswd(
body,
'test',
'somerandompassword',
'newPassword',
defaultHashConfig
);
} catch (error: any) {
expect(error.message).toEqual(
`Unable to change password for user 'test': invalid old password`
);
}
});
test('should throw error when user does not exist', async () => {
const body = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z';
try {
await changePasswordToHTPasswd(
body,
'test2',
'somerandompassword',
'newPassword',
defaultHashConfig
);
} catch (error: any) {
expect(error.message).toEqual(
`Unable to change password for user 'test2': user does not currently exist`
);
}
});
test('should change the password', async () => {
const body = 'root:$6qLTHoPfGLy2:autocreated 2018-08-20T13:38:12.164Z';
expect(
await changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword', defaultHashConfig)
).toMatchSnapshot();
});
});