From aeff267d940d224916af1dd41bc92e1d9326cb97 Mon Sep 17 00:00:00 2001 From: Ed Clement Date: Thu, 3 Mar 2022 15:57:19 -0500 Subject: [PATCH] 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 --- .changeset/swift-pumpkins-knock.md | 6 + packages/auth/src/auth.ts | 2 +- packages/plugins/htpasswd/README.md | 19 +++ packages/plugins/htpasswd/src/htpasswd.ts | 67 +++++----- packages/plugins/htpasswd/src/utils.ts | 53 ++++---- .../htpasswd/tests/__fixtures__/htpasswd | 1 + .../htpasswd/tests/__mocks__/Logger.ts | 1 - .../plugins/htpasswd/tests/htpasswd.test.ts | 118 +++++++++++------- packages/plugins/htpasswd/tests/utils.test.ts | 99 +++++++++------ 9 files changed, 228 insertions(+), 138 deletions(-) create mode 100644 .changeset/swift-pumpkins-knock.md delete mode 100644 packages/plugins/htpasswd/tests/__mocks__/Logger.ts diff --git a/.changeset/swift-pumpkins-knock.md b/.changeset/swift-pumpkins-knock.md new file mode 100644 index 000000000..eca1d97df --- /dev/null +++ b/.changeset/swift-pumpkins-knock.md @@ -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. diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 9fb136c5c..2ee660a71 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -111,7 +111,7 @@ class Auth implements IAuth { }; let authPlugin; try { - authPlugin = new HTPasswd(plugingConf, pluginOptions); + authPlugin = new HTPasswd(plugingConf, pluginOptions as any as PluginOptions); } catch (error: any) { debug('error on loading auth htpasswd plugin stack: %o', error); return []; diff --git a/packages/plugins/htpasswd/README.md b/packages/plugins/htpasswd/README.md index 7caff08f4..c512b162c 100644 --- a/packages/plugins/htpasswd/README.md +++ b/packages/plugins/htpasswd/README.md @@ -30,7 +30,26 @@ As simple as running: # Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt". #algorithm: bcrypt # Rounds number for "bcrypt", will be ignored for other algorithms. + # Setting this value higher will result in password verification taking longer. #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 diff --git a/packages/plugins/htpasswd/src/htpasswd.ts b/packages/plugins/htpasswd/src/htpasswd.ts index ee7469453..6e7f6829d 100644 --- a/packages/plugins/htpasswd/src/htpasswd.ts +++ b/packages/plugins/htpasswd/src/htpasswd.ts @@ -19,9 +19,12 @@ export type HTPasswdConfig = { file: string; algorithm?: HtpasswdHashAlgorithm; rounds?: number; + max_users?: number; + slow_verify_ms?: number; } & Config; export const DEFAULT_BCRYPT_ROUNDS = 10; +export const DEFAULT_SLOW_VERIFY_MS = 200; /** * HTPasswd - Verdaccio auth class @@ -30,30 +33,21 @@ export default class HTPasswd implements IPluginAuth { /** * * @param {*} config htpasswd file - * @param {object} stuff config.yaml in object from + * @param {object} options config.yaml in object from */ private users: {}; - private stuff: {}; - private config: {}; - private verdaccioConfig: Config; private maxUsers: number; private hashConfig: HtpasswdHashConfig; private path: string; + private slowVerifyMs: number; private logger: Logger; private lastTime: any; // constructor - public constructor(config: HTPasswdConfig, stuff: PluginOptions<{}>) { + public constructor(config: HTPasswdConfig, options: PluginOptions) { this.users = {}; - // config for this module - this.config = config; - this.stuff = stuff; - // verdaccio logger - this.logger = stuff.logger; - - // verdaccio main config object - this.verdaccioConfig = stuff.config; + this.logger = options.logger; // all this "verdaccio_config" stuff is for b/w compatibility only this.maxUsers = config.max_users ? config.max_users : Infinity; @@ -88,25 +82,41 @@ export default class HTPasswd implements IPluginAuth { 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. * @param {string} user * @param {string} password - * @param {function} cd - * @returns {function} + * @param {function} cb + * @returns {void} */ public authenticate(user: string, password: string, cb: Callback): void { - this.reload((err) => { + this.reload(async (err) => { if (err) { return cb(err.code === 'ENOENT' ? null : err); } if (!this.users[user]) { 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); } @@ -130,11 +140,11 @@ export default class HTPasswd implements IPluginAuth { * @param {string} user * @param {string} password * @param {function} realCb - * @returns {function} + * @returns {Promise} */ - public adduser(user: string, password: string, realCb: Callback): any { + public async adduser(user: string, password: string, realCb: Callback): Promise { 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 // not needed @@ -142,7 +152,7 @@ export default class HTPasswd implements IPluginAuth { return realCb(sanity, false); } - lockAndRead(pathPass, (err, res): void => { + lockAndRead(pathPass, async (err, res): Promise => { let locked = false; // callback that cleans up lock first @@ -170,7 +180,7 @@ export default class HTPasswd implements IPluginAuth { // real checks, to prevent race conditions // 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) { return cb(sanity); @@ -230,7 +240,8 @@ export default class HTPasswd implements IPluginAuth { * changePassword - change password for existing user. * @param {string} user * @param {string} password - * @param {function} cd + * @param {string} newPassword + * @param {function} realCb * @returns {function} */ public changePassword( @@ -239,7 +250,7 @@ export default class HTPasswd implements IPluginAuth { newPassword: string, realCb: Callback ): void { - lockAndRead(this.path, (err, res) => { + lockAndRead(this.path, async (err, res) => { let locked = false; const pathPassFile = this.path; @@ -266,13 +277,9 @@ export default class HTPasswd implements IPluginAuth { const body = this._stringToUt8(res); this.users = parseHTPasswd(body); - if (!this.users[user]) { - return cb(new Error('User not found')); - } - try { this._writeFile( - changePasswordToHTPasswd(body, user, password, newPassword, this.hashConfig), + await changePasswordToHTPasswd(body, user, password, newPassword, this.hashConfig), cb ); } catch (err: any) { diff --git a/packages/plugins/htpasswd/src/utils.ts b/packages/plugins/htpasswd/src/utils.ts index 958b0c6e2..4a78927fc 100644 --- a/packages/plugins/htpasswd/src/utils.ts +++ b/packages/plugins/htpasswd/src/utils.ts @@ -39,8 +39,9 @@ export function lockAndRead(name: string, cb: Callback): void { * @returns {object} */ export function parseHTPasswd(input: string): Record { - return input.split('\n').reduce((result, line) => { - const args = line.split(':', 3); + // The input is split on line ending styles that are both windows and unix compatible + return input.split(/[\r]?[\n]/).reduce((result, line) => { + const args = line.split(':', 3).map((str) => str.trim()); if (args.length > 1) { result[args[0]] = args[1]; } @@ -52,11 +53,13 @@ export function parseHTPasswd(input: string): Record { * verifyPassword - matches password and it's hash. * @param {string} passwd * @param {string} hash - * @returns {boolean} + * @returns {Promise} */ -export function verifyPassword(passwd: string, hash: string): boolean { - if (hash.match(/^\$2(a|b|y)\$/)) { - return bcrypt.compareSync(passwd, hash); +export async function verifyPassword(passwd: string, hash: string): Promise { + if (hash.match(/^\$2([aby])\$/)) { + return new Promise((resolve, reject) => + bcrypt.compare(passwd, hash, (error, result) => (error ? reject(error) : resolve(result))) + ); } else if (hash.indexOf('{PLAIN}') === 0) { return passwd === hash.substr(7); } else if (hash.indexOf('{SHA}') === 0) { @@ -112,6 +115,7 @@ export function generateHtpasswdLine( * @param {string} body * @param {string} user * @param {string} passwd + * @param {HtpasswdHashConfig} hashConfig * @returns {string} */ export function addUserToHTPasswd( @@ -139,16 +143,18 @@ export function addUserToHTPasswd( * Sanity check for a user * @param {string} user * @param {object} users + * @param {string} password + * @param {Callback} verifyFn * @param {number} maxUsers * @returns {object} */ -export function sanityCheck( +export async function sanityCheck( user: string, password: string, verifyFn: Callback, users: {}, maxUsers: number -): HttpError | null { +): Promise { let err; // check for user or password @@ -167,7 +173,7 @@ export function sanityCheck( } if (hash) { - const auth = verifyFn(password, users[user]); + const auth = await verifyFn(password, users[user]); if (auth) { err = Error(API_ERROR.USERNAME_ALREADY_REGISTERED); err.status = HTTP_STATUS.CONFLICT; @@ -191,28 +197,27 @@ export function sanityCheck( * @param {string} user * @param {string} passwd * @param {string} newPasswd + * @param {HtpasswdHashConfig} hashConfig * @returns {string} */ -export function changePasswordToHTPasswd( +export async function changePasswordToHTPasswd( body: string, user: string, passwd: string, newPasswd: string, hashConfig: HtpasswdHashConfig -): string { +): Promise { let lines = body.split('\n'); - lines = lines.map((line) => { - const [username, hash] = line.split(':', 3); - - if (username === user) { - if (verifyPassword(passwd, hash)) { - line = generateHtpasswdLine(user, newPasswd, hashConfig); - } else { - throw new Error('Invalid old Password'); - } - } - return line; - }); - + const userLineIndex = lines.findIndex((line) => line.split(':', 1).shift() === user); + if (userLineIndex === -1) { + throw new Error(`Unable to change password for user '${user}': user does not currently exist`); + } + 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`); + } + const updatedUserLine = generateHtpasswdLine(username, newPasswd, hashConfig); + lines.splice(userLineIndex, 1, updatedUserLine); return lines.join('\n'); } diff --git a/packages/plugins/htpasswd/tests/__fixtures__/htpasswd b/packages/plugins/htpasswd/tests/__fixtures__/htpasswd index dda54a931..5c8a14a3f 100644 --- a/packages/plugins/htpasswd/tests/__fixtures__/htpasswd +++ b/packages/plugins/htpasswd/tests/__fixtures__/htpasswd @@ -1,2 +1,3 @@ test:$6FrCaT/v0dwE:autocreated 2018-01-17T03:40:22.958Z username:$66to3JK5RgZM:autocreated 2018-01-17T03:40:46.315Z +bcrypt:$2y$04$K2Cn3StiXx4CnLmcTW/ymekOrj7WlycZZF9xgmoJ/U0zGPqSLPVBe diff --git a/packages/plugins/htpasswd/tests/__mocks__/Logger.ts b/packages/plugins/htpasswd/tests/__mocks__/Logger.ts deleted file mode 100644 index 18e0b8c4a..000000000 --- a/packages/plugins/htpasswd/tests/__mocks__/Logger.ts +++ /dev/null @@ -1 +0,0 @@ -export default class Logger {} diff --git a/packages/plugins/htpasswd/tests/htpasswd.test.ts b/packages/plugins/htpasswd/tests/htpasswd.test.ts index 6f34180ff..50203d7d5 100644 --- a/packages/plugins/htpasswd/tests/htpasswd.test.ts +++ b/packages/plugins/htpasswd/tests/htpasswd.test.ts @@ -1,36 +1,35 @@ /* 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'; -// @ts-ignore +// @ts-ignore: Module has no default export import fs from 'fs'; 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 Config from './__mocks__/Config'; -// FIXME: remove this mocks imports -import Logger from './__mocks__/Logger'; -const stuff = { - logger: new Logger(), +const options = { + logger: { warn: jest.fn() }, config: new Config(), -}; +} as any as PluginOptions; const config = { file: './htpasswd', max_users: 1000, -}; - -const getDefaultConfig = (): VerdaccioConfigApp => ({ - file: './htpasswd', - max_users: 1000, -}); +} as HTPasswdConfig; describe('HTPasswd', () => { let wrapper; beforeEach(() => { - wrapper = new HTPasswd(getDefaultConfig(), stuff as unknown as VerdaccioConfigApp); + wrapper = new HTPasswd(config, options); jest.resetModules(); + jest.clearAllMocks(); crypto.randomBytes = jest.fn(() => { return { @@ -40,46 +39,71 @@ describe('HTPasswd', () => { }); describe('constructor()', () => { - const emptyPluginOptions = { config: {} } as VerdaccioConfigApp; + const emptyPluginOptions = { config: {} } as any as PluginOptions; - test('should files whether file path does not exist', () => { + test('should ensure file path configuration exists', () => { expect(function () { - new HTPasswd({}, emptyPluginOptions); + new HTPasswd({} as HTPasswdConfig, emptyPluginOptions); }).toThrow(/should specify "file" in config/); }); test('should throw error about incorrect algorithm', () => { expect(function () { - let config = getDefaultConfig(); - config.algorithm = 'invalid' as any; - new HTPasswd(config, emptyPluginOptions); + let invalidConfig = { algorithm: 'invalid', ...config } as HTPasswdConfig; + new HTPasswd(invalidConfig, emptyPluginOptions); }).toThrow(/Invalid algorithm "invalid"/); }); }); describe('authenticate()', () => { test('it should authenticate user with given credentials', (done) => { - const callbackTest = (a, b): void => { - expect(a).toBeNull(); - expect(b).toContain('test'); - done(); + const users = [ + { username: 'test', password: 'test' }, + { username: 'username', password: 'password' }, + { 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 => { - expect(a).toBeNull(); - expect(b).toContain('username'); - done(); - }; - wrapper.authenticate('test', 'test', callbackTest); - wrapper.authenticate('username', 'password', callbackUsername); + users.forEach(({ username, password }) => + wrapper.authenticate(username, password, generateCallback(username)) + ); }); 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 => { expect(a).toBeNull(); - expect(b).toBeFalsy(); + expect(b).toContain('bcrypt'); + const mockWarn = options.logger.warn as jest.MockedFn; + 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(); }; - wrapper.authenticate('test', 'somerandompassword', callback); + wrapper.authenticate('bcrypt', 'password', callback); }); }); @@ -122,7 +146,7 @@ describe('HTPasswd', () => { }); const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.adduser('sanityCheck', 'test', (sanity) => { expect(sanity.message).toBeDefined(); expect(sanity.message).toMatch('some error'); @@ -140,7 +164,7 @@ describe('HTPasswd', () => { }); const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.adduser('lockAndRead', 'test', (sanity) => { expect(sanity.message).toBeDefined(); expect(sanity.message).toMatch('lock error'); @@ -160,7 +184,7 @@ describe('HTPasswd', () => { }); const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.adduser('addUserToHTPasswd', 'test', () => { done(); }); @@ -187,7 +211,7 @@ describe('HTPasswd', () => { }); const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.adduser('addUserToHTPasswd', 'test', (err) => { expect(err).not.toBeNull(); expect(err.message).toMatch('write error'); @@ -198,7 +222,11 @@ describe('HTPasswd', () => { describe('reload()', () => { 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 => { expect(wrapper.users).toEqual(output); done(); @@ -224,7 +252,7 @@ describe('HTPasswd', () => { }; const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.reload(callback); }); @@ -247,7 +275,7 @@ describe('HTPasswd', () => { }; const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.reload(callback); }); @@ -267,7 +295,7 @@ describe('HTPasswd', () => { }; const HTPasswd = require('../src/htpasswd.ts').default; - const wrapper = new HTPasswd(config, stuff); + const wrapper = new HTPasswd(config, options); wrapper.reload(callback); }); }); @@ -276,7 +304,9 @@ describe('HTPasswd', () => { test('changePassword - it should throw an error for user not found', (done) => { const callback = (error, isSuccess): void => { 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(); done(); }; @@ -286,7 +316,9 @@ describe('HTPasswd', () => { test('changePassword - it should throw an error for wrong password', (done) => { const callback = (error, isSuccess): void => { 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(); done(); }; diff --git a/packages/plugins/htpasswd/tests/utils.test.ts b/packages/plugins/htpasswd/tests/utils.test.ts index 11752261f..d6cb0bb86 100644 --- a/packages/plugins/htpasswd/tests/utils.test.ts +++ b/packages/plugins/htpasswd/tests/utils.test.ts @@ -1,3 +1,4 @@ +// @ts-ignore: Module has no default export import crypto from 'crypto'; import MockDate from 'mockdate'; @@ -66,40 +67,40 @@ user4:$6FrCasdvppdwE:autocreated 2017-12-14T13:30:20.838Z`; }); 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']; - 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']; - 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']; - 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']; - 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=']; - 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=']; - 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']; - 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 = [ 'testpasswordchanged', '$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' }; }); - test('should throw error for user already exists', () => { + test('should throw error for user already exists', async () => { 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.message).toEqual('unauthorized access'); 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 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.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 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.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 input = sanityCheck('username', users.test, verifyFn, users, 2); + const input = await sanityCheck('username', users.test, verifyFn, users, 2); expect(input).toBeNull(); }); - test('should throw error for required username field', () => { + test('should throw error for required username field', async () => { 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.status).toEqual(400); }); - test('should throw error for required password field', () => { + test('should throw error for required password field', async () => { 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.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 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.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 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.message).toEqual('username is already registered'); expect(verifyFn).toHaveBeenCalledTimes(1); @@ -229,9 +230,9 @@ describe('sanityCheck', () => { test( 'should throw error for existing username and password with max number ' + 'of users reached', - () => { + async () => { 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.message).toEqual('username is already registered'); expect(verifyFn).toHaveBeenCalledTimes(1); @@ -240,11 +241,11 @@ describe('sanityCheck', () => { }); 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'; try { - changePasswordToHTPasswd( + await changePasswordToHTPasswd( body, 'test', 'somerandompassword', @@ -252,15 +253,35 @@ describe('changePasswordToHTPasswd', () => { defaultHashConfig ); } 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'; expect( - changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword', defaultHashConfig) + await changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword', defaultHashConfig) ).toMatchSnapshot(); }); });