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:
parent
8adee83d51
commit
62c24b6321
13
.changeset/shy-ducks-cover.md
Normal file
13
.changeset/shy-ducks-cover.md
Normal file
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user