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;
try {
authPlugin = new HTPasswd(plugingConf, pluginOptions);
authPlugin = new HTPasswd(plugingConf, pluginOptions as any as PluginOptions<HTPasswdConfig>);
} catch (error: any) {
debug('error on loading auth htpasswd plugin stack: %o', error);
return [];

View file

@ -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

View file

@ -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<HTPasswdConfig> {
/**
*
* @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<HTPasswdConfig>) {
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<HTPasswdConfig> {
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<HTPasswdConfig> {
* @param {string} user
* @param {string} password
* @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;
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<HTPasswdConfig> {
return realCb(sanity, false);
}
lockAndRead(pathPass, (err, res): void => {
lockAndRead(pathPass, async (err, res): Promise<void> => {
let locked = false;
// callback that cleans up lock first
@ -170,7 +180,7 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
// 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<HTPasswdConfig> {
* 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<HTPasswdConfig> {
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<HTPasswdConfig> {
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) {

View file

@ -39,8 +39,9 @@ export function lockAndRead(name: string, cb: Callback): void {
* @returns {object}
*/
export function parseHTPasswd(input: string): Record<string, any> {
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<string, any> {
* verifyPassword - matches password and it's hash.
* @param {string} passwd
* @param {string} hash
* @returns {boolean}
* @returns {Promise<boolean>}
*/
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<boolean> {
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<HttpError | null> {
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<string> {
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');
}

View file

@ -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

View file

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

View file

@ -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<HTPasswdConfig>;
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<HTPasswdConfig>;
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<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();
};
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();
};

View file

@ -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();
});
});