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;
|
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 [];
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export default class Logger {}
|
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue