1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-20 17:05:52 +01:00

feat: add passwordValidationRegex property (#3384)

* feat: add passwordValidationRegex property

* chore: improve validation

* Update validation-utilts.spec.ts

* Update validation-utilts.spec.ts

* chore: remove code
This commit is contained in:
Juan Picado 2022-09-17 19:29:40 +02:00 committed by GitHub
parent 8adee83d51
commit 62c24b6321
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 288 additions and 382 deletions

@ -0,0 +1,13 @@
---
'@verdaccio/api': minor
'@verdaccio/auth': minor
'@verdaccio/config': minor
'@verdaccio/core': minor
'@verdaccio/types': minor
'@verdaccio/server-fastify': minor
'@verdaccio/store': minor
'@verdaccio/utils': minor
'@verdaccio/web': minor
---
feat: add passwordValidationRegex property

@ -52,7 +52,7 @@ export default function (config: Config, auth: IAuth, storage: Storage): Router
// for "npm whoami"
whoami(app);
pkg(app, auth, storage);
profile(app, auth);
profile(app, auth, config);
// @deprecated endpoint, 404 by default
search(app);
user(app, auth, config);

@ -1,14 +1,13 @@
import buildDebug from 'debug';
import { Response, Router } from 'express';
import _ from 'lodash';
import { getApiToken } from '@verdaccio/auth';
import { IAuth } from '@verdaccio/auth';
import { createRemoteUser } from '@verdaccio/config';
import { API_ERROR, API_MESSAGE, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import { API_ERROR, API_MESSAGE, HTTP_STATUS, errorUtils, validatioUtils } from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { Config, RemoteUser } from '@verdaccio/types';
import { getAuthenticatedMessage, mask, validatePassword } from '@verdaccio/utils';
import { getAuthenticatedMessage, mask } from '@verdaccio/utils';
import { $NextFunctionVer, $RequestExtend } from '../types/custom';
@ -50,9 +49,9 @@ export default function (route: Router, auth: IAuth, config: Config): void {
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
const { name, password } = req.body;
debug('login or adduser');
const remoteName = req.remote_user.name;
const remoteName = req?.remote_user?.name;
if (_.isNil(remoteName) === false && _.isNil(name) === false && remoteName === name) {
if (typeof remoteName !== 'undefined' && typeof name === 'string' && remoteName === name) {
debug('login: no remote user detected');
auth.authenticate(
name,
@ -87,10 +86,15 @@ export default function (route: Router, auth: IAuth, config: Config): void {
}
);
} else {
if (validatePassword(password) === false) {
if (
validatioUtils.validatePassword(
password,
config?.serverSettings?.passwordValidationRegex
) === false
) {
debug('adduser: invalid password');
// eslint-disable-next-line new-cap
return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT()));
return next(errorUtils.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT));
}
auth.add_user(name, password, async function (err, user): Promise<void> {
@ -109,7 +113,9 @@ export default function (route: Router, auth: IAuth, config: Config): void {
const token =
name && password ? await getApiToken(auth, config, user, password) : undefined;
debug('adduser: new token %o', mask(token as string, 4));
if (token) {
debug('adduser: new token %o', mask(token as string, 4));
}
if (!token) {
return next(errorUtils.getUnauthorized());
}

@ -2,8 +2,15 @@ import { Response, Router } from 'express';
import _ from 'lodash';
import { IAuth } from '@verdaccio/auth';
import { API_ERROR, APP_ERROR, HTTP_STATUS, SUPPORT_ERRORS, errorUtils } from '@verdaccio/core';
import { validatePassword } from '@verdaccio/utils';
import {
API_ERROR,
APP_ERROR,
HTTP_STATUS,
SUPPORT_ERRORS,
errorUtils,
validatioUtils,
} from '@verdaccio/core';
import { Config } from '@verdaccio/types';
import { $NextFunctionVer, $RequestExtend } from '../../types/custom';
@ -18,7 +25,7 @@ export interface Profile {
fullname: string;
}
export default function (route: Router, auth: IAuth): void {
export default function (route: Router, auth: IAuth, config: Config): void {
function buildProfile(name: string): Profile {
return {
tfa: false,
@ -60,9 +67,14 @@ export default function (route: Router, auth: IAuth): void {
const { name } = req.remote_user;
if (_.isNil(password) === false) {
if (validatePassword(password.new) === false) {
if (
validatioUtils.validatePassword(
password.new,
config?.serverSettings?.passwordValidationRegex
) === false
) {
/* eslint new-cap:off */
return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT()));
return next(errorUtils.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT));
/* eslint new-cap:off */
}

@ -1,80 +0,0 @@
import supertest from 'supertest';
import { API_ERROR, HEADERS, HEADER_TYPE, HTTP_STATUS, TOKEN_BEARER } from '@verdaccio/core';
import { buildToken } from '@verdaccio/utils';
import { createUser, getPackage, initializeServer } from './_helper';
const FORBIDDEN_VUE = 'authorization required to access package vue';
jest.setTimeout(20000);
describe('token', () => {
describe('basics', () => {
const FAKE_TOKEN: string = buildToken(TOKEN_BEARER, 'fake');
test.each([['user.yaml'], ['user.jwt.yaml']])('should test add a new user', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const vueResponse = await getPackage(app, response.body.token, 'vue');
expect(vueResponse.body).toBeDefined();
expect(vueResponse.body.name).toMatch('vue');
const vueFailResp = await getPackage(app, FAKE_TOKEN, 'vue', HTTP_STATUS.UNAUTHORIZED);
expect(vueFailResp.body.error).toMatch(FORBIDDEN_VUE);
});
test.each([['user.yaml'], ['user.jwt.yaml']])('should test add a new user', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: credentials.password,
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CONFLICT);
expect(response2.body.error).toBe(API_ERROR.USERNAME_ALREADY_REGISTERED);
});
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should fails on login if user credentials are invalid even if jwt',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'newFailsUser', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: 'BAD_PASSWORD',
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.UNAUTHORIZED);
expect(response2.body.error).toBe(API_ERROR.UNAUTHORIZED_ACCESS);
}
);
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should verify if user is logged',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.get(`/-/user/org.couchdb.user:${credentials.name}`)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, response.body.token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response2.body.ok).toBe(`you are authenticated as '${credentials.name}'`);
}
);
});
});

@ -1,267 +1,169 @@
import _ from 'lodash';
import supertest from 'supertest';
import {
API_ERROR,
API_MESSAGE,
HEADERS,
HEADER_TYPE,
HTTP_STATUS,
errorUtils,
} from '@verdaccio/core';
import { API_ERROR, HEADERS, HEADER_TYPE, HTTP_STATUS, TOKEN_BEARER } from '@verdaccio/core';
import { buildToken } from '@verdaccio/utils';
import { $RequestExtend, $ResponseExtend } from '../../types/custom';
import { initializeServer } from './_helper';
import { createUser, getPackage, initializeServer } from './_helper';
const mockApiJWTmiddleware = jest.fn(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: 'test', groups: [], real_groups: [] };
_next();
}
);
const FORBIDDEN_VUE = 'authorization required to access package vue';
const mockAuthenticate = jest.fn(() => (_name, _password, callback): void => {
return callback(null, ['all']);
});
jest.setTimeout(20000);
const mockAddUser = jest.fn(() => (_name, _password, callback): void => {
return callback(errorUtils.getConflict(API_ERROR.USERNAME_ALREADY_REGISTERED));
});
describe('token', () => {
describe('basics', () => {
const FAKE_TOKEN: string = buildToken(TOKEN_BEARER, 'fake');
test.each([['user.yaml'], ['user.jwt.yaml']])('should test add a new user', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
jest.mock('@verdaccio/auth', () => ({
getApiToken: () => 'token',
Auth: class {
apiJWTmiddleware() {
return mockApiJWTmiddleware();
}
init() {
return Promise.resolve();
}
allow_access(_d, f_, cb) {
cb(null, true);
}
add_user(name, password, callback) {
mockAddUser()(name, password, callback);
}
authenticate(_name, _password, callback) {
mockAuthenticate()(_name, _password, callback);
}
},
}));
const vueResponse = await getPackage(app, response.body.token, 'vue');
expect(vueResponse.body).toBeDefined();
expect(vueResponse.body.name).toMatch('vue');
// FIXME: This might be covered with user.jwt.spec
describe('user', () => {
const credentials = { name: 'test', password: 'test' };
test('should test add a new user', async () => {
mockApiJWTmiddleware.mockImplementationOnce(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: undefined };
_next();
}
);
mockAddUser.mockImplementationOnce(() => (_name, _password, callback): void => {
return callback(null, true);
const vueFailResp = await getPackage(app, FAKE_TOKEN, 'vue', HTTP_STATUS.UNAUTHORIZED);
expect(vueFailResp.body.error).toMatch(FORBIDDEN_VUE);
});
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.put(`/-/user/org.couchdb.user:newUser`)
test.each([['user.yaml'], ['user.jwt.yaml']])('should login an user', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'test', password: 'test' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: 'newUser',
password: 'newUser',
name: credentials.name,
password: credentials.password,
})
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, response.body.token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED)
.end(function (err, res) {
if (err) {
return reject(err);
}
expect(res.body.ok).toBeDefined();
expect(res.body.token).toBeDefined();
const token = res.body.token;
expect(typeof token).toBe('string');
expect(res.body.ok).toMatch(`user 'newUser' created`);
resolve(null);
});
.expect(HTTP_STATUS.CREATED);
});
});
test('should test fails on add a existing user with login', async () => {
mockApiJWTmiddleware.mockImplementationOnce(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: undefined };
_next();
}
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should fails login a valid user',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'test', password: 'test' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: 'failPassword',
})
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, response.body.token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.UNAUTHORIZED);
}
);
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.put('/-/user/org.couchdb.user:jotaNew')
.send(credentials)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CONFLICT)
.end(function (err, res) {
if (err) {
return reject(err);
}
expect(res.body.error).toBeDefined();
expect(res.body.error).toMatch(API_ERROR.USERNAME_ALREADY_REGISTERED);
resolve(res.body);
});
});
});
test('should log in as existing user', async () => {
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send(credentials)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED)
.end((err, res) => {
if (err) {
return reject(err);
}
expect(res.body).toBeTruthy();
expect(res.body.ok).toMatch(`you are authenticated as \'${credentials.name}\'`);
resolve(res);
});
});
});
test('should test fails add a new user with missing name', async () => {
mockApiJWTmiddleware.mockImplementationOnce(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: undefined };
_next();
}
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should test conflict create new user',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: credentials.password,
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CONFLICT);
expect(response2.body.error).toBe(API_ERROR.USERNAME_ALREADY_REGISTERED);
}
);
mockAddUser.mockImplementationOnce(() => (_name, _password, callback): void => {
return callback(errorUtils.getBadRequest(API_ERROR.USERNAME_PASSWORD_REQUIRED));
});
const credentialsShort = _.cloneDeep(credentials);
delete credentialsShort.name;
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send(credentialsShort)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.BAD_REQUEST)
.end(function (err, res) {
if (err) {
return reject(err);
}
expect(res.body.error).toBeDefined();
expect(res.body.error).toMatch(API_ERROR.USERNAME_PASSWORD_REQUIRED);
resolve(app);
});
});
});
test('should test fails add a new user with missing password', async () => {
mockApiJWTmiddleware.mockImplementationOnce(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: undefined };
_next();
}
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should fails on login if user credentials are invalid',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'newFailsUser', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: 'BAD_PASSWORD',
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.UNAUTHORIZED);
expect(response2.body.error).toBe(API_ERROR.UNAUTHORIZED_ACCESS);
}
);
const credentialsShort = _.cloneDeep(credentials);
delete credentialsShort.password;
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send(credentialsShort)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.BAD_REQUEST)
.end(function (err, res) {
if (err) {
return reject(err);
}
expect(res.body.error).toBeDefined();
// FIXME: message is not 100% accurate
// eslint-disable-next-line new-cap
expect(res.body.error).toMatch(API_ERROR.PASSWORD_SHORT());
resolve(res);
});
});
});
test('should test fails add a new user with wrong password', async () => {
mockApiJWTmiddleware.mockImplementationOnce(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: 'test' };
_next();
}
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should fails password validation',
async (conf) => {
const credentials = { name: 'test', password: '12' };
const app = await initializeServer(conf);
const response = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: credentials.password,
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.BAD_REQUEST);
expect(response.body.error).toBe(API_ERROR.PASSWORD_SHORT);
}
);
mockAuthenticate.mockImplementationOnce(() => (_name, _password, callback): void => {
return callback(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));
});
const credentialsShort = _.cloneDeep(credentials);
credentialsShort.password = 'failPassword';
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.put('/-/user/org.couchdb.user:test')
.send(credentialsShort)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.UNAUTHORIZED)
.end(function (err, res) {
if (err) {
return reject(err);
}
expect(res.body.error).toBeDefined();
expect(res.body.error).toMatch(API_ERROR.BAD_USERNAME_PASSWORD);
resolve(res);
});
});
});
test('should be able to logout an user', async () => {
mockApiJWTmiddleware.mockImplementationOnce(
() =>
(req: $RequestExtend, _res: $ResponseExtend, _next): void => {
req.remote_user = { name: 'test' };
_next();
}
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should fails missing password validation',
async (conf) => {
const credentials = { name: 'test' };
const app = await initializeServer(conf);
const response = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: undefined,
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.BAD_REQUEST);
expect(response.body.error).toBe(API_ERROR.PASSWORD_SHORT);
}
);
mockAuthenticate.mockImplementationOnce(() => (_name, _password, callback): void => {
return callback(errorUtils.getUnauthorized(API_ERROR.BAD_USERNAME_PASSWORD));
});
const credentialsShort = _.cloneDeep(credentials);
credentialsShort.password = 'failPassword';
const app = await initializeServer('user.yaml');
return new Promise((resolve, reject) => {
supertest(app)
.delete('/-/user/token/someSecretToken')
.send(credentialsShort)
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should verify if user is logged',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.get(`/-/user/org.couchdb.user:${credentials.name}`)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, response.body.token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response2.body.ok).toBe(`you are authenticated as '${credentials.name}'`);
}
);
test.each([['user.yaml'], ['user.jwt.yaml']])('should logout user', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
await supertest(app)
.get(`/-/user/org.couchdb.user:${credentials.name}`)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, response.body.token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK)
.end(function (err, res) {
if (err) {
return reject(err);
}
expect(res.body.ok).toMatch(API_MESSAGE.LOGGED_OUT);
resolve(res);
});
.expect(HTTP_STATUS.OK);
await supertest(app)
.delete(`/-/user/token/someSecretToken:${response.body.token}`)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
});
});
});

@ -142,7 +142,7 @@ class Auth implements IAuth {
// @ts-ignore
return authenticate || allow_access || allow_publish;
},
this.config?.server?.pluginPrefix
this.config?.serverSettings?.pluginPrefix
);
}

@ -103,6 +103,9 @@ server:
# The pluginPrefix replaces the default plugins prefix which is `verdaccio`, please don't include `-`. If `something` is provided
# the resolve package will be `something-xxxx`.
# pluginPrefix: something
# A regex for the password validation /.{3}$/ (3 characters min)
# An example to limit to 10 characters minimum
# passwordValidationRegex: /.{10}$/
# https://verdaccio.org/docs/configuration#offline-publish
# publish:

@ -109,6 +109,9 @@ server:
# The pluginPrefix replaces the default plugins prefix which is `verdaccio`, please don't include `-`. If `something` is provided
# the resolve package will be `something-xxxx`.
# pluginPrefix: something
# A regex for the password validation /.{3}$/ (3 characters min)
# An example to limit to 10 characters minimum
# passwordValidationRegex: /.{10}$/
# https://verdaccio.org/docs/configuration#offline-publish
# publish:

@ -1,6 +1,6 @@
import httpCodes from 'http-status-codes';
export const DEFAULT_MIN_LIMIT_PASSWORD = 3;
export const DEFAULT_PASSWORD_VALIDATION = /.{3}$/;
export const TIME_EXPIRATION_24H = '24h';
export const TIME_EXPIRATION_7D = '7d';
export const DIST_TAGS = 'dist-tags';

@ -1,11 +1,9 @@
import createError, { HttpError } from 'http-errors';
import { DEFAULT_MIN_LIMIT_PASSWORD, HTTP_STATUS } from './constants';
import { HTTP_STATUS } from './constants';
export const API_ERROR = {
PASSWORD_SHORT: (passLength = DEFAULT_MIN_LIMIT_PASSWORD): string =>
`The provided password is too short. Please pick a password longer than ` +
`${passLength} characters.`,
PASSWORD_SHORT: `The provided password does not pass the validation`,
MUST_BE_LOGGED: 'You must be logged in to publish packages.',
PLUGIN_ERROR: 'bug in the auth plugin system',
CONFIG_BAD_FORMAT: 'config file must be an object',

@ -20,7 +20,7 @@ export {
CHARACTER_ENCODING,
HEADER_TYPE,
LATEST,
DEFAULT_MIN_LIMIT_PASSWORD,
DEFAULT_PASSWORD_VALIDATION,
DEFAULT_USER,
USERS,
} from './constants';

@ -2,7 +2,7 @@ import assert from 'assert';
import { Manifest } from '@verdaccio/types';
import { DIST_TAGS } from './constants';
import { DEFAULT_PASSWORD_VALIDATION, DIST_TAGS } from './constants';
export { validatePublishSingleVersion } from './schemes/publish-manifest';
@ -104,3 +104,12 @@ export function isObject(obj: any): boolean {
Array.isArray(obj) === false
);
}
export function validatePassword(
password: string,
validation: RegExp = DEFAULT_PASSWORD_VALIDATION
): boolean {
return typeof password === 'string' && validation instanceof RegExp
? password.match(validation) !== null
: false;
}

@ -1,10 +1,11 @@
import { DIST_TAGS } from '../src/constants';
import { DEFAULT_PASSWORD_VALIDATION, DIST_TAGS } from '../src/constants';
import { validatePublishSingleVersion } from '../src/schemes/publish-manifest';
import {
isObject,
normalizeMetadata,
validateName,
validatePackage,
validatePassword,
} from '../src/validation-utils';
describe('validatePackage', () => {
@ -166,3 +167,53 @@ describe('validatePublishSingleVersion', () => {
).toBeFalsy();
});
});
describe('validatePassword', () => {
test('should validate password according the length', () => {
expect(validatePassword('12345', DEFAULT_PASSWORD_VALIDATION)).toBeTruthy();
});
test('should validate invalid regex', () => {
// @ts-expect-error
expect(validatePassword('12345', 34234342)).toBeFalsy();
});
test('should validate invalid regex (undefined)', () => {
expect(validatePassword('12345', undefined)).toBeTruthy();
});
test('should validate invalid password)', () => {
// @ts-expect-error
expect(validatePassword(undefined)).toBeFalsy();
});
test('should validate invalid password number)', () => {
// @ts-expect-error
expect(validatePassword(2342344234342)).toBeFalsy();
});
test('should fails on validate password according the length', () => {
expect(validatePassword('12345', /.{10}$/)).toBeFalsy();
});
test('should fails handle complex password validation', () => {
expect(validatePassword('12345', /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/)).toBeFalsy();
});
test('should handle complex password validation', () => {
expect(
validatePassword(
'c<?_:srdsj&WyZgY}r4:l[F<RgV<}',
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/
)
).toBeTruthy();
});
test('should fails on validate password according the length and default config', () => {
expect(validatePassword('12')).toBeFalsy();
});
test('should validate password according the length and default config', () => {
expect(validatePassword('1235678910')).toBeTruthy();
});
});

@ -233,6 +233,7 @@ export type ServerSettingsConf = {
* acme-XXXXXX
*/
pluginPrefix?: string;
passwordValidationRegex?: RegExp;
};
/**

@ -7,9 +7,10 @@ import _ from 'lodash';
import { getApiToken } from '@verdaccio/auth';
import { createRemoteUser } from '@verdaccio/config';
import { validatioUtils } from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { RemoteUser } from '@verdaccio/types';
import { getAuthenticatedMessage, validatePassword } from '@verdaccio/utils';
import { getAuthenticatedMessage } from '@verdaccio/utils';
const debug = buildDebug('verdaccio:fastify:user');
@ -96,13 +97,18 @@ async function userRoute(fastify: FastifyInstance) {
}
);
} else {
if (validatePassword(password) === false) {
if (
validatioUtils.validatePassword(
password as string,
fastify.configInstance?.server?.passwordValidationRegex
) === false
) {
debug('adduser: invalid password');
reply.code(fastify.statusCode.BAD_REQUEST).send(
fastify.errorUtils.getCode(
fastify.statusCode.BAD_REQUEST,
// eslint-disable-next-line new-cap
fastify.apiError.PASSWORD_SHORT()
fastify.apiError.PASSWORD_SHORT
)
);
return;

@ -2,8 +2,8 @@ import buildDebug from 'debug';
import { FastifyInstance } from 'fastify';
import _ from 'lodash';
import { validatioUtils } from '@verdaccio/core';
import { JWTSignOptions } from '@verdaccio/types';
import { validatePassword } from '@verdaccio/utils';
const debug = buildDebug('verdaccio:fastify:web:login');
const loginBodySchema = {
@ -77,7 +77,12 @@ async function loginRoute(fastify: FastifyInstance) {
const { password } = request.body;
const { name } = request.userRemote;
if (validatePassword(password.new) === false) {
if (
validatioUtils.validatePassword(
password.new,
fastify.configInstance?.server?.passwordValidationRegex
) === false
) {
fastify.auth.changePassword(
name as string,
password.old,

@ -75,7 +75,7 @@ class LocalStorage {
(plugin): IPluginStorage => {
return plugin.getPackageStorage;
},
this.config?.server?.pluginPrefix
this.config?.serverSettings?.pluginPrefix
);
if (plugins.length > 1) {

@ -677,7 +677,7 @@ class Storage {
(plugin) => {
return plugin.filterMetadata;
},
this.config?.server?.pluginPrefix
this.config?.serverSettings?.pluginPrefix
);
debug('filters available %o', this.filters);
}

@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 61,
lines: 60,
},
},
});

@ -1,16 +1,7 @@
import { DEFAULT_MIN_LIMIT_PASSWORD } from '@verdaccio/core';
export interface CookieSessionToken {
expires: Date;
}
export function validatePassword(
password: string,
minLength: number = DEFAULT_MIN_LIMIT_PASSWORD
): boolean {
return typeof password === 'string' && password.length >= minLength;
}
export function createSessionToken(): CookieSessionToken {
const tenHoursTime = 10 * 60 * 60 * 1000;

@ -1,24 +1,6 @@
import { createSessionToken, getAuthenticatedMessage, validatePassword } from '../src';
import { createSessionToken, getAuthenticatedMessage } from '../src';
describe('Auth Utilities', () => {
describe('validatePassword', () => {
test('should validate password according the length', () => {
expect(validatePassword('12345', 1)).toBeTruthy();
});
test('should fails on validate password according the length', () => {
expect(validatePassword('12345', 10)).toBeFalsy();
});
test('should fails on validate password according the length and default config', () => {
expect(validatePassword('12')).toBeFalsy();
});
test('should validate password according the length and default config', () => {
expect(validatePassword('1235678910')).toBeTruthy();
});
});
describe('createSessionToken', () => {
test('should generate session token', () => {
expect(createSessionToken()).toHaveProperty('expires');

@ -3,9 +3,8 @@ import { Request, Response, Router } from 'express';
import _ from 'lodash';
import { IAuth } from '@verdaccio/auth';
import { API_ERROR, APP_ERROR, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import { API_ERROR, APP_ERROR, HTTP_STATUS, errorUtils, validatioUtils } from '@verdaccio/core';
import { Config, JWTSignOptions, RemoteUser } from '@verdaccio/types';
import { validatePassword } from '@verdaccio/utils';
import { $NextFunctionVer } from './package';
@ -48,7 +47,12 @@ function addUserAuthApi(auth: IAuth, config: Config): Router {
const { password } = req.body;
const { name } = req.remote_user;
if (validatePassword(password.new) === false) {
if (
validatioUtils.validatePassword(
password.new,
config?.serverSettings?.passwordValidationRegex
) === false
) {
auth.changePassword(
name as string,
password.old,

@ -22,7 +22,7 @@ export async function loadTheme(config: any) {
function (plugin: string) {
return typeof plugin === 'string';
},
config?.server?.pluginPrefix ?? 'verdaccio-theme'
config?.serverSettings?.pluginPrefix ?? 'verdaccio-theme'
);
if (plugin.length > 1) {
logger.warn(