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:
parent
a0dca6e927
commit
aeff267d94
9 changed files with 228 additions and 138 deletions
6
.changeset/swift-pumpkins-knock.md
Normal file
6
.changeset/swift-pumpkins-knock.md
Normal 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.
|
|
@ -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 [];
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export default class Logger {}
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue