0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

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
This commit is contained in:
Ed Clement 2022-03-03 15:57:19 -05:00 committed by GitHub
parent a0dca6e927
commit aeff267d94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 138 deletions

View file

@ -0,0 +1,6 @@
---
'@verdaccio/auth': patch
'verdaccio-htpasswd': patch
---
Refactor htpasswd plugin to use the bcryptjs 'compare' api call instead of 'comparSync'. Add a new configuration value named 'slow_verify_ms' to the htpasswd plugin that when exceeded during password verification will log a warning message.

View file

@ -111,7 +111,7 @@ class Auth implements IAuth {
}; };
let authPlugin; let authPlugin;
try { try {
authPlugin = new HTPasswd(plugingConf, pluginOptions); authPlugin = new HTPasswd(plugingConf, pluginOptions as any as PluginOptions<HTPasswdConfig>);
} catch (error: any) { } catch (error: any) {
debug('error on loading auth htpasswd plugin stack: %o', error); debug('error on loading auth htpasswd plugin stack: %o', error);
return []; return [];

View file

@ -30,7 +30,26 @@ As simple as running:
# Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt". # Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt".
#algorithm: bcrypt #algorithm: bcrypt
# Rounds number for "bcrypt", will be ignored for other algorithms. # Rounds number for "bcrypt", will be ignored for other algorithms.
# Setting this value higher will result in password verification taking longer.
#rounds: 10 #rounds: 10
# Log a warning if the password takes more then this duration in milliseconds to verify.
#slow_verify_ms: 200
### Bcrypt rounds
It is important to note that when using the default `bcrypt` algorithm and setting
the `rounds` configuration value to a higher number then the default of `10`, that
verification of a user password can cause significantly increased CPU usage and
additional latency in processing requests.
If your Verdaccio instance handles a large number of authenticated requests using
username and password for authentication, the `rounds` configuration value may need
to be decreased to prevent excessive CPU usage and request latency.
Also note that setting the `rounds` configuration value to a value that is too small
increases the risk of successful brute force attack. Auth0 has a
[blog article](https://auth0.com/blog/hashing-in-action-understanding-bcrypt)
that provides an overview of how `bcrypt` hashing works and some best practices.
## Logging In ## Logging In

View file

@ -19,9 +19,12 @@ export type HTPasswdConfig = {
file: string; file: string;
algorithm?: HtpasswdHashAlgorithm; algorithm?: HtpasswdHashAlgorithm;
rounds?: number; rounds?: number;
max_users?: number;
slow_verify_ms?: number;
} & Config; } & Config;
export const DEFAULT_BCRYPT_ROUNDS = 10; export const DEFAULT_BCRYPT_ROUNDS = 10;
export const DEFAULT_SLOW_VERIFY_MS = 200;
/** /**
* HTPasswd - Verdaccio auth class * HTPasswd - Verdaccio auth class
@ -30,30 +33,21 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
/** /**
* *
* @param {*} config htpasswd file * @param {*} config htpasswd file
* @param {object} stuff config.yaml in object from * @param {object} options config.yaml in object from
*/ */
private users: {}; private users: {};
private stuff: {};
private config: {};
private verdaccioConfig: Config;
private maxUsers: number; private maxUsers: number;
private hashConfig: HtpasswdHashConfig; private hashConfig: HtpasswdHashConfig;
private path: string; private path: string;
private slowVerifyMs: number;
private logger: Logger; private logger: Logger;
private lastTime: any; private lastTime: any;
// constructor // constructor
public constructor(config: HTPasswdConfig, stuff: PluginOptions<{}>) { public constructor(config: HTPasswdConfig, options: PluginOptions<HTPasswdConfig>) {
this.users = {}; this.users = {};
// config for this module
this.config = config;
this.stuff = stuff;
// verdaccio logger // verdaccio logger
this.logger = stuff.logger; this.logger = options.logger;
// verdaccio main config object
this.verdaccioConfig = stuff.config;
// all this "verdaccio_config" stuff is for b/w compatibility only // all this "verdaccio_config" stuff is for b/w compatibility only
this.maxUsers = config.max_users ? config.max_users : Infinity; this.maxUsers = config.max_users ? config.max_users : Infinity;
@ -88,25 +82,41 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
throw new Error('should specify "file" in config'); throw new Error('should specify "file" in config');
} }
this.path = Path.resolve(Path.dirname(this.verdaccioConfig.config_path), file); this.path = Path.resolve(Path.dirname(options.config.config_path), file);
this.slowVerifyMs = config.slow_verify_ms || DEFAULT_SLOW_VERIFY_MS;
} }
/** /**
* authenticate - Authenticate user. * authenticate - Authenticate user.
* @param {string} user * @param {string} user
* @param {string} password * @param {string} password
* @param {function} cd * @param {function} cb
* @returns {function} * @returns {void}
*/ */
public authenticate(user: string, password: string, cb: Callback): void { public authenticate(user: string, password: string, cb: Callback): void {
this.reload((err) => { this.reload(async (err) => {
if (err) { if (err) {
return cb(err.code === 'ENOENT' ? null : err); return cb(err.code === 'ENOENT' ? null : err);
} }
if (!this.users[user]) { if (!this.users[user]) {
return cb(null, false); return cb(null, false);
} }
if (!verifyPassword(password, this.users[user])) {
let passwordValid = false;
try {
const start = new Date();
passwordValid = await verifyPassword(password, this.users[user]);
const durationMs = new Date().getTime() - start.getTime();
if (durationMs > this.slowVerifyMs) {
this.logger.warn(
{ user, durationMs },
'Password for user "@{user}" took @{durationMs}ms to verify'
);
}
} catch ({ message }) {
this.logger.error({ message }, 'Unable to verify user password: @{message}');
}
if (!passwordValid) {
return cb(null, false); return cb(null, false);
} }
@ -130,11 +140,11 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
* @param {string} user * @param {string} user
* @param {string} password * @param {string} password
* @param {function} realCb * @param {function} realCb
* @returns {function} * @returns {Promise<any>}
*/ */
public adduser(user: string, password: string, realCb: Callback): any { public async adduser(user: string, password: string, realCb: Callback): Promise<any> {
const pathPass = this.path; const pathPass = this.path;
let sanity = sanityCheck(user, password, verifyPassword, this.users, this.maxUsers); let sanity = await sanityCheck(user, password, verifyPassword, this.users, this.maxUsers);
// preliminary checks, just to ensure that file won't be reloaded if it's // preliminary checks, just to ensure that file won't be reloaded if it's
// not needed // not needed
@ -142,7 +152,7 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
return realCb(sanity, false); return realCb(sanity, false);
} }
lockAndRead(pathPass, (err, res): void => { lockAndRead(pathPass, async (err, res): Promise<void> => {
let locked = false; let locked = false;
// callback that cleans up lock first // callback that cleans up lock first
@ -170,7 +180,7 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
// real checks, to prevent race conditions // real checks, to prevent race conditions
// parsing users after reading file. // parsing users after reading file.
sanity = sanityCheck(user, password, verifyPassword, this.users, this.maxUsers); sanity = await sanityCheck(user, password, verifyPassword, this.users, this.maxUsers);
if (sanity) { if (sanity) {
return cb(sanity); return cb(sanity);
@ -230,7 +240,8 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
* changePassword - change password for existing user. * changePassword - change password for existing user.
* @param {string} user * @param {string} user
* @param {string} password * @param {string} password
* @param {function} cd * @param {string} newPassword
* @param {function} realCb
* @returns {function} * @returns {function}
*/ */
public changePassword( public changePassword(
@ -239,7 +250,7 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
newPassword: string, newPassword: string,
realCb: Callback realCb: Callback
): void { ): void {
lockAndRead(this.path, (err, res) => { lockAndRead(this.path, async (err, res) => {
let locked = false; let locked = false;
const pathPassFile = this.path; const pathPassFile = this.path;
@ -266,13 +277,9 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
const body = this._stringToUt8(res); const body = this._stringToUt8(res);
this.users = parseHTPasswd(body); this.users = parseHTPasswd(body);
if (!this.users[user]) {
return cb(new Error('User not found'));
}
try { try {
this._writeFile( this._writeFile(
changePasswordToHTPasswd(body, user, password, newPassword, this.hashConfig), await changePasswordToHTPasswd(body, user, password, newPassword, this.hashConfig),
cb cb
); );
} catch (err: any) { } catch (err: any) {

View file

@ -39,8 +39,9 @@ export function lockAndRead(name: string, cb: Callback): void {
* @returns {object} * @returns {object}
*/ */
export function parseHTPasswd(input: string): Record<string, any> { export function parseHTPasswd(input: string): Record<string, any> {
return input.split('\n').reduce((result, line) => { // The input is split on line ending styles that are both windows and unix compatible
const args = line.split(':', 3); return input.split(/[\r]?[\n]/).reduce((result, line) => {
const args = line.split(':', 3).map((str) => str.trim());
if (args.length > 1) { if (args.length > 1) {
result[args[0]] = args[1]; result[args[0]] = args[1];
} }
@ -52,11 +53,13 @@ export function parseHTPasswd(input: string): Record<string, any> {
* verifyPassword - matches password and it's hash. * verifyPassword - matches password and it's hash.
* @param {string} passwd * @param {string} passwd
* @param {string} hash * @param {string} hash
* @returns {boolean} * @returns {Promise<boolean>}
*/ */
export function verifyPassword(passwd: string, hash: string): boolean { export async function verifyPassword(passwd: string, hash: string): Promise<boolean> {
if (hash.match(/^\$2(a|b|y)\$/)) { if (hash.match(/^\$2([aby])\$/)) {
return bcrypt.compareSync(passwd, hash); return new Promise((resolve, reject) =>
bcrypt.compare(passwd, hash, (error, result) => (error ? reject(error) : resolve(result)))
);
} else if (hash.indexOf('{PLAIN}') === 0) { } else if (hash.indexOf('{PLAIN}') === 0) {
return passwd === hash.substr(7); return passwd === hash.substr(7);
} else if (hash.indexOf('{SHA}') === 0) { } else if (hash.indexOf('{SHA}') === 0) {
@ -112,6 +115,7 @@ export function generateHtpasswdLine(
* @param {string} body * @param {string} body
* @param {string} user * @param {string} user
* @param {string} passwd * @param {string} passwd
* @param {HtpasswdHashConfig} hashConfig
* @returns {string} * @returns {string}
*/ */
export function addUserToHTPasswd( export function addUserToHTPasswd(
@ -139,16 +143,18 @@ export function addUserToHTPasswd(
* Sanity check for a user * Sanity check for a user
* @param {string} user * @param {string} user
* @param {object} users * @param {object} users
* @param {string} password
* @param {Callback} verifyFn
* @param {number} maxUsers * @param {number} maxUsers
* @returns {object} * @returns {object}
*/ */
export function sanityCheck( export async function sanityCheck(
user: string, user: string,
password: string, password: string,
verifyFn: Callback, verifyFn: Callback,
users: {}, users: {},
maxUsers: number maxUsers: number
): HttpError | null { ): Promise<HttpError | null> {
let err; let err;
// check for user or password // check for user or password
@ -167,7 +173,7 @@ export function sanityCheck(
} }
if (hash) { if (hash) {
const auth = verifyFn(password, users[user]); const auth = await verifyFn(password, users[user]);
if (auth) { if (auth) {
err = Error(API_ERROR.USERNAME_ALREADY_REGISTERED); err = Error(API_ERROR.USERNAME_ALREADY_REGISTERED);
err.status = HTTP_STATUS.CONFLICT; err.status = HTTP_STATUS.CONFLICT;
@ -191,28 +197,27 @@ export function sanityCheck(
* @param {string} user * @param {string} user
* @param {string} passwd * @param {string} passwd
* @param {string} newPasswd * @param {string} newPasswd
* @param {HtpasswdHashConfig} hashConfig
* @returns {string} * @returns {string}
*/ */
export function changePasswordToHTPasswd( export async function changePasswordToHTPasswd(
body: string, body: string,
user: string, user: string,
passwd: string, passwd: string,
newPasswd: string, newPasswd: string,
hashConfig: HtpasswdHashConfig hashConfig: HtpasswdHashConfig
): string { ): Promise<string> {
let lines = body.split('\n'); let lines = body.split('\n');
lines = lines.map((line) => { const userLineIndex = lines.findIndex((line) => line.split(':', 1).shift() === user);
const [username, hash] = line.split(':', 3); if (userLineIndex === -1) {
throw new Error(`Unable to change password for user '${user}': user does not currently exist`);
if (username === user) {
if (verifyPassword(passwd, hash)) {
line = generateHtpasswdLine(user, newPasswd, hashConfig);
} else {
throw new Error('Invalid old Password');
} }
const [username, hash] = lines[userLineIndex].split(':', 2);
const passwordValid = await verifyPassword(passwd, hash);
if (!passwordValid) {
throw new Error(`Unable to change password for user '${user}': invalid old password`);
} }
return line; const updatedUserLine = generateHtpasswdLine(username, newPasswd, hashConfig);
}); lines.splice(userLineIndex, 1, updatedUserLine);
return lines.join('\n'); return lines.join('\n');
} }

View file

@ -1,2 +1,3 @@
test:$6FrCaT/v0dwE:autocreated 2018-01-17T03:40:22.958Z test:$6FrCaT/v0dwE:autocreated 2018-01-17T03:40:22.958Z
username:$66to3JK5RgZM:autocreated 2018-01-17T03:40:46.315Z username:$66to3JK5RgZM:autocreated 2018-01-17T03:40:46.315Z
bcrypt:$2y$04$K2Cn3StiXx4CnLmcTW/ymekOrj7WlycZZF9xgmoJ/U0zGPqSLPVBe

View file

@ -1 +0,0 @@
export default class Logger {}

View file

@ -1,36 +1,35 @@
/* eslint-disable jest/no-mocks-import */ /* eslint-disable jest/no-mocks-import */
// @ts-ignore: Module has no default export
import bcrypt from 'bcryptjs';
// @ts-ignore: Module has no default export
import crypto from 'crypto'; import crypto from 'crypto';
// @ts-ignore // @ts-ignore: Module has no default export
import fs from 'fs'; import fs from 'fs';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
import HTPasswd, { VerdaccioConfigApp } from '../src/htpasswd'; import { PluginOptions } from '@verdaccio/types';
import HTPasswd, { DEFAULT_SLOW_VERIFY_MS, HTPasswdConfig } from '../src/htpasswd';
import { HtpasswdHashAlgorithm } from '../src/utils'; import { HtpasswdHashAlgorithm } from '../src/utils';
import Config from './__mocks__/Config'; import Config from './__mocks__/Config';
// FIXME: remove this mocks imports
import Logger from './__mocks__/Logger';
const stuff = { const options = {
logger: new Logger(), logger: { warn: jest.fn() },
config: new Config(), config: new Config(),
}; } as any as PluginOptions<HTPasswdConfig>;
const config = { const config = {
file: './htpasswd', file: './htpasswd',
max_users: 1000, max_users: 1000,
}; } as HTPasswdConfig;
const getDefaultConfig = (): VerdaccioConfigApp => ({
file: './htpasswd',
max_users: 1000,
});
describe('HTPasswd', () => { describe('HTPasswd', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
wrapper = new HTPasswd(getDefaultConfig(), stuff as unknown as VerdaccioConfigApp); wrapper = new HTPasswd(config, options);
jest.resetModules(); jest.resetModules();
jest.clearAllMocks();
crypto.randomBytes = jest.fn(() => { crypto.randomBytes = jest.fn(() => {
return { return {
@ -40,46 +39,71 @@ describe('HTPasswd', () => {
}); });
describe('constructor()', () => { describe('constructor()', () => {
const emptyPluginOptions = { config: {} } as VerdaccioConfigApp; const emptyPluginOptions = { config: {} } as any as PluginOptions<HTPasswdConfig>;
test('should files whether file path does not exist', () => { test('should ensure file path configuration exists', () => {
expect(function () { expect(function () {
new HTPasswd({}, emptyPluginOptions); new HTPasswd({} as HTPasswdConfig, emptyPluginOptions);
}).toThrow(/should specify "file" in config/); }).toThrow(/should specify "file" in config/);
}); });
test('should throw error about incorrect algorithm', () => { test('should throw error about incorrect algorithm', () => {
expect(function () { expect(function () {
let config = getDefaultConfig(); let invalidConfig = { algorithm: 'invalid', ...config } as HTPasswdConfig;
config.algorithm = 'invalid' as any; new HTPasswd(invalidConfig, emptyPluginOptions);
new HTPasswd(config, emptyPluginOptions);
}).toThrow(/Invalid algorithm "invalid"/); }).toThrow(/Invalid algorithm "invalid"/);
}); });
}); });
describe('authenticate()', () => { describe('authenticate()', () => {
test('it should authenticate user with given credentials', (done) => { test('it should authenticate user with given credentials', (done) => {
const callbackTest = (a, b): void => { const users = [
expect(a).toBeNull(); { username: 'test', password: 'test' },
expect(b).toContain('test'); { username: 'username', password: 'password' },
done(); { username: 'bcrypt', password: 'password' },
];
let usersAuthenticated = 0;
const generateCallback = (username) => (error, userGroups) => {
usersAuthenticated += 1;
expect(error).toBeNull();
expect(userGroups).toContain(username);
usersAuthenticated === users.length && done();
}; };
const callbackUsername = (a, b): void => { users.forEach(({ username, password }) =>
expect(a).toBeNull(); wrapper.authenticate(username, password, generateCallback(username))
expect(b).toContain('username'); );
done();
};
wrapper.authenticate('test', 'test', callbackTest);
wrapper.authenticate('username', 'password', callbackUsername);
}); });
test('it should not authenticate user with given credentials', (done) => { test('it should not authenticate user with given credentials', (done) => {
const users = ['test', 'username', 'bcrypt'];
let usersAuthenticated = 0;
const generateCallback = () => (error, userGroups) => {
usersAuthenticated += 1;
expect(error).toBeNull();
expect(userGroups).toBeFalsy();
usersAuthenticated === users.length && done();
};
users.forEach((username) =>
wrapper.authenticate(username, 'somerandompassword', generateCallback())
);
});
test('it should warn on slow password verification', (done) => {
bcrypt.compare = jest.fn((passwd, hash, callback) => {
setTimeout(() => callback(null, true), DEFAULT_SLOW_VERIFY_MS + 1);
});
const callback = (a, b): void => { const callback = (a, b): void => {
expect(a).toBeNull(); expect(a).toBeNull();
expect(b).toBeFalsy(); expect(b).toContain('bcrypt');
const mockWarn = options.logger.warn as jest.MockedFn<jest.MockableFunction>;
expect(mockWarn.mock.calls.length).toBe(1);
const [{ user, durationMs }, message] = mockWarn.mock.calls[0];
expect(user).toEqual('bcrypt');
expect(durationMs).toBeGreaterThan(DEFAULT_SLOW_VERIFY_MS);
expect(message).toEqual('Password for user "@{user}" took @{durationMs}ms to verify');
done(); done();
}; };
wrapper.authenticate('test', 'somerandompassword', callback); wrapper.authenticate('bcrypt', 'password', callback);
}); });
}); });
@ -122,7 +146,7 @@ describe('HTPasswd', () => {
}); });
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.adduser('sanityCheck', 'test', (sanity) => { wrapper.adduser('sanityCheck', 'test', (sanity) => {
expect(sanity.message).toBeDefined(); expect(sanity.message).toBeDefined();
expect(sanity.message).toMatch('some error'); expect(sanity.message).toMatch('some error');
@ -140,7 +164,7 @@ describe('HTPasswd', () => {
}); });
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.adduser('lockAndRead', 'test', (sanity) => { wrapper.adduser('lockAndRead', 'test', (sanity) => {
expect(sanity.message).toBeDefined(); expect(sanity.message).toBeDefined();
expect(sanity.message).toMatch('lock error'); expect(sanity.message).toMatch('lock error');
@ -160,7 +184,7 @@ describe('HTPasswd', () => {
}); });
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.adduser('addUserToHTPasswd', 'test', () => { wrapper.adduser('addUserToHTPasswd', 'test', () => {
done(); done();
}); });
@ -187,7 +211,7 @@ describe('HTPasswd', () => {
}); });
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.adduser('addUserToHTPasswd', 'test', (err) => { wrapper.adduser('addUserToHTPasswd', 'test', (err) => {
expect(err).not.toBeNull(); expect(err).not.toBeNull();
expect(err.message).toMatch('write error'); expect(err.message).toMatch('write error');
@ -198,7 +222,11 @@ describe('HTPasswd', () => {
describe('reload()', () => { describe('reload()', () => {
test('it should read the file and set the users', (done) => { test('it should read the file and set the users', (done) => {
const output = { test: '$6FrCaT/v0dwE', username: '$66to3JK5RgZM' }; const output = {
test: '$6FrCaT/v0dwE',
username: '$66to3JK5RgZM',
bcrypt: '$2y$04$K2Cn3StiXx4CnLmcTW/ymekOrj7WlycZZF9xgmoJ/U0zGPqSLPVBe',
};
const callback = (): void => { const callback = (): void => {
expect(wrapper.users).toEqual(output); expect(wrapper.users).toEqual(output);
done(); done();
@ -224,7 +252,7 @@ describe('HTPasswd', () => {
}; };
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.reload(callback); wrapper.reload(callback);
}); });
@ -247,7 +275,7 @@ describe('HTPasswd', () => {
}; };
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.reload(callback); wrapper.reload(callback);
}); });
@ -267,7 +295,7 @@ describe('HTPasswd', () => {
}; };
const HTPasswd = require('../src/htpasswd.ts').default; const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff); const wrapper = new HTPasswd(config, options);
wrapper.reload(callback); wrapper.reload(callback);
}); });
}); });
@ -276,7 +304,9 @@ describe('HTPasswd', () => {
test('changePassword - it should throw an error for user not found', (done) => { test('changePassword - it should throw an error for user not found', (done) => {
const callback = (error, isSuccess): void => { const callback = (error, isSuccess): void => {
expect(error).not.toBeNull(); expect(error).not.toBeNull();
expect(error.message).toBe('User not found'); expect(error.message).toBe(
`Unable to change password for user 'usernotpresent': user does not currently exist`
);
expect(isSuccess).toBeFalsy(); expect(isSuccess).toBeFalsy();
done(); done();
}; };
@ -286,7 +316,9 @@ describe('HTPasswd', () => {
test('changePassword - it should throw an error for wrong password', (done) => { test('changePassword - it should throw an error for wrong password', (done) => {
const callback = (error, isSuccess): void => { const callback = (error, isSuccess): void => {
expect(error).not.toBeNull(); expect(error).not.toBeNull();
expect(error.message).toBe('Invalid old Password'); expect(error.message).toBe(
`Unable to change password for user 'username': invalid old password`
);
expect(isSuccess).toBeFalsy(); expect(isSuccess).toBeFalsy();
done(); done();
}; };

View file

@ -1,3 +1,4 @@
// @ts-ignore: Module has no default export
import crypto from 'crypto'; import crypto from 'crypto';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
@ -66,40 +67,40 @@ user4:$6FrCasdvppdwE:autocreated 2017-12-14T13:30:20.838Z`;
}); });
describe('verifyPassword', () => { describe('verifyPassword', () => {
it('should verify the MD5/Crypt3 password with true', () => { it('should verify the MD5/Crypt3 password with true', async () => {
const input = ['test', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0']; const input = ['test', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0'];
expect(verifyPassword(input[0], input[1])).toBeTruthy(); expect(await verifyPassword(input[0], input[1])).toBeTruthy();
}); });
it('should verify the MD5/Crypt3 password with false', () => { it('should verify the MD5/Crypt3 password with false', async () => {
const input = ['testpasswordchanged', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0']; const input = ['testpasswordchanged', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0'];
expect(verifyPassword(input[0], input[1])).toBeFalsy(); expect(await verifyPassword(input[0], input[1])).toBeFalsy();
}); });
it('should verify the plain password with true', () => { it('should verify the plain password with true', async () => {
const input = ['testpasswordchanged', '{PLAIN}testpasswordchanged']; const input = ['testpasswordchanged', '{PLAIN}testpasswordchanged'];
expect(verifyPassword(input[0], input[1])).toBeTruthy(); expect(await verifyPassword(input[0], input[1])).toBeTruthy();
}); });
it('should verify the plain password with false', () => { it('should verify the plain password with false', async () => {
const input = ['testpassword', '{PLAIN}testpasswordchanged']; const input = ['testpassword', '{PLAIN}testpasswordchanged'];
expect(verifyPassword(input[0], input[1])).toBeFalsy(); expect(await verifyPassword(input[0], input[1])).toBeFalsy();
}); });
it('should verify the crypto SHA password with true', () => { it('should verify the crypto SHA password with true', async () => {
const input = ['testpassword', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0=']; const input = ['testpassword', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0='];
expect(verifyPassword(input[0], input[1])).toBeTruthy(); expect(await verifyPassword(input[0], input[1])).toBeTruthy();
}); });
it('should verify the crypto SHA password with false', () => { it('should verify the crypto SHA password with false', async () => {
const input = ['testpasswordchanged', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0=']; const input = ['testpasswordchanged', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0='];
expect(verifyPassword(input[0], input[1])).toBeFalsy(); expect(await verifyPassword(input[0], input[1])).toBeFalsy();
}); });
it('should verify the bcrypt password with true', () => { it('should verify the bcrypt password with true', async () => {
const input = ['testpassword', '$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO']; const input = ['testpassword', '$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO'];
expect(verifyPassword(input[0], input[1])).toBeTruthy(); expect(await verifyPassword(input[0], input[1])).toBeTruthy();
}); });
it('should verify the bcrypt password with false', () => { it('should verify the bcrypt password with false', async () => {
const input = [ const input = [
'testpasswordchanged', 'testpasswordchanged',
'$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO', '$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO',
]; ];
expect(verifyPassword(input[0], input[1])).toBeFalsy(); expect(await verifyPassword(input[0], input[1])).toBeFalsy();
}); });
}); });
@ -170,58 +171,58 @@ describe('sanityCheck', () => {
users = { test: '$6FrCaT/v0dwE' }; users = { test: '$6FrCaT/v0dwE' };
}); });
test('should throw error for user already exists', () => { test('should throw error for user already exists', async () => {
const verifyFn = jest.fn(); const verifyFn = jest.fn();
const input = sanityCheck('test', users.test, verifyFn, users, Infinity); const input = await sanityCheck('test', users.test, verifyFn, users, Infinity);
expect(input.status).toEqual(401); expect(input.status).toEqual(401);
expect(input.message).toEqual('unauthorized access'); expect(input.message).toEqual('unauthorized access');
expect(verifyFn).toHaveBeenCalled(); expect(verifyFn).toHaveBeenCalled();
}); });
test('should throw error for registration disabled of users', () => { test('should throw error for registration disabled of users', async () => {
const verifyFn = (): void => {}; const verifyFn = (): void => {};
const input = sanityCheck('username', users.test, verifyFn, users, -1); const input = await sanityCheck('username', users.test, verifyFn, users, -1);
expect(input.status).toEqual(409); expect(input.status).toEqual(409);
expect(input.message).toEqual('user registration disabled'); expect(input.message).toEqual('user registration disabled');
}); });
test('should throw error max number of users', () => { test('should throw error max number of users', async () => {
const verifyFn = (): void => {}; const verifyFn = (): void => {};
const input = sanityCheck('username', users.test, verifyFn, users, 1); const input = await sanityCheck('username', users.test, verifyFn, users, 1);
expect(input.status).toEqual(403); expect(input.status).toEqual(403);
expect(input.message).toEqual('maximum amount of users reached'); expect(input.message).toEqual('maximum amount of users reached');
}); });
test('should not throw anything and sanity check', () => { test('should not throw anything and sanity check', async () => {
const verifyFn = (): void => {}; const verifyFn = (): void => {};
const input = sanityCheck('username', users.test, verifyFn, users, 2); const input = await sanityCheck('username', users.test, verifyFn, users, 2);
expect(input).toBeNull(); expect(input).toBeNull();
}); });
test('should throw error for required username field', () => { test('should throw error for required username field', async () => {
const verifyFn = (): void => {}; const verifyFn = (): void => {};
const input = sanityCheck(undefined, users.test, verifyFn, users, 2); const input = await sanityCheck(undefined, users.test, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required'); expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400); expect(input.status).toEqual(400);
}); });
test('should throw error for required password field', () => { test('should throw error for required password field', async () => {
const verifyFn = (): void => {}; const verifyFn = (): void => {};
const input = sanityCheck('username', undefined, verifyFn, users, 2); const input = await sanityCheck('username', undefined, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required'); expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400); expect(input.status).toEqual(400);
}); });
test('should throw error for required username & password fields', () => { test('should throw error for required username & password fields', async () => {
const verifyFn = (): void => {}; const verifyFn = (): void => {};
const input = sanityCheck(undefined, undefined, verifyFn, users, 2); const input = await sanityCheck(undefined, undefined, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required'); expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400); expect(input.status).toEqual(400);
}); });
test('should throw error for existing username and password', () => { test('should throw error for existing username and password', async () => {
const verifyFn = jest.fn(() => true); const verifyFn = jest.fn(() => true);
const input = sanityCheck('test', users.test, verifyFn, users, 2); const input = await sanityCheck('test', users.test, verifyFn, users, 2);
expect(input.status).toEqual(409); expect(input.status).toEqual(409);
expect(input.message).toEqual('username is already registered'); expect(input.message).toEqual('username is already registered');
expect(verifyFn).toHaveBeenCalledTimes(1); expect(verifyFn).toHaveBeenCalledTimes(1);
@ -229,9 +230,9 @@ describe('sanityCheck', () => {
test( test(
'should throw error for existing username and password with max number ' + 'of users reached', 'should throw error for existing username and password with max number ' + 'of users reached',
() => { async () => {
const verifyFn = jest.fn(() => true); const verifyFn = jest.fn(() => true);
const input = sanityCheck('test', users.test, verifyFn, users, 1); const input = await sanityCheck('test', users.test, verifyFn, users, 1);
expect(input.status).toEqual(409); expect(input.status).toEqual(409);
expect(input.message).toEqual('username is already registered'); expect(input.message).toEqual('username is already registered');
expect(verifyFn).toHaveBeenCalledTimes(1); expect(verifyFn).toHaveBeenCalledTimes(1);
@ -240,11 +241,11 @@ describe('sanityCheck', () => {
}); });
describe('changePasswordToHTPasswd', () => { describe('changePasswordToHTPasswd', () => {
test('should throw error for wrong password', () => { test('should throw error for wrong password', async () => {
const body = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z'; const body = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z';
try { try {
changePasswordToHTPasswd( await changePasswordToHTPasswd(
body, body,
'test', 'test',
'somerandompassword', 'somerandompassword',
@ -252,15 +253,35 @@ describe('changePasswordToHTPasswd', () => {
defaultHashConfig defaultHashConfig
); );
} catch (error: any) { } catch (error: any) {
expect(error.message).toEqual('Invalid old Password'); expect(error.message).toEqual(
`Unable to change password for user 'test': invalid old password`
);
} }
}); });
test('should change the 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'; const body = 'root:$6qLTHoPfGLy2:autocreated 2018-08-20T13:38:12.164Z';
expect( expect(
changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword', defaultHashConfig) await changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword', defaultHashConfig)
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
}); });