From 62c24b63215d6cc56255785fd40462450dd82fca Mon Sep 17 00:00:00 2001 From: Juan Picado Date: Sat, 17 Sep 2022 19:29:40 +0200 Subject: [PATCH] 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 --- .changeset/shy-ducks-cover.md | 13 + packages/api/src/index.ts | 2 +- packages/api/src/user.ts | 22 +- packages/api/src/v1/profile.ts | 22 +- .../api/test/integration/user.jwt.spec.ts | 80 ---- packages/api/test/integration/user.spec.ts | 380 +++++++----------- packages/auth/src/auth.ts | 2 +- packages/config/src/conf/default.yaml | 3 + packages/config/src/conf/docker.yaml | 3 + packages/core/core/src/constants.ts | 2 +- packages/core/core/src/error-utils.ts | 6 +- packages/core/core/src/index.ts | 2 +- packages/core/core/src/validation-utils.ts | 11 +- .../core/core/test/validation-utilts.spec.ts | 53 ++- packages/core/types/src/configuration.ts | 1 + packages/server/fastify/src/endpoints/user.ts | 12 +- .../fastify/src/routes/web/api/login.ts | 9 +- packages/store/src/local-storage.ts | 2 +- packages/store/src/storage.ts | 2 +- packages/utils/jest.config.js | 2 +- packages/utils/src/auth-utils.ts | 9 - packages/utils/test/auth-utils.spec.ts | 20 +- packages/web/src/api/user.ts | 10 +- packages/web/src/middleware/render-web.ts | 2 +- 24 files changed, 288 insertions(+), 382 deletions(-) create mode 100644 .changeset/shy-ducks-cover.md delete mode 100644 packages/api/test/integration/user.jwt.spec.ts diff --git a/.changeset/shy-ducks-cover.md b/.changeset/shy-ducks-cover.md new file mode 100644 index 000000000..2342fdbef --- /dev/null +++ b/.changeset/shy-ducks-cover.md @@ -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 diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 3ae2c0f80..e6787cbb6 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -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); diff --git a/packages/api/src/user.ts b/packages/api/src/user.ts index ec0bfe5cb..84122f1ed 100644 --- a/packages/api/src/user.ts +++ b/packages/api/src/user.ts @@ -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 { @@ -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()); } diff --git a/packages/api/src/v1/profile.ts b/packages/api/src/v1/profile.ts index bd75b924c..875166149 100644 --- a/packages/api/src/v1/profile.ts +++ b/packages/api/src/v1/profile.ts @@ -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 */ } diff --git a/packages/api/test/integration/user.jwt.spec.ts b/packages/api/test/integration/user.jwt.spec.ts deleted file mode 100644 index bb79c5e4e..000000000 --- a/packages/api/test/integration/user.jwt.spec.ts +++ /dev/null @@ -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}'`); - } - ); - }); -}); diff --git a/packages/api/test/integration/user.spec.ts b/packages/api/test/integration/user.spec.ts index edd3fc21d..8f4746982 100644 --- a/packages/api/test/integration/user.spec.ts +++ b/packages/api/test/integration/user.spec.ts @@ -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); }); }); }); diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index b543f9317..29b5e29e1 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -142,7 +142,7 @@ class Auth implements IAuth { // @ts-ignore return authenticate || allow_access || allow_publish; }, - this.config?.server?.pluginPrefix + this.config?.serverSettings?.pluginPrefix ); } diff --git a/packages/config/src/conf/default.yaml b/packages/config/src/conf/default.yaml index 12f653b36..75bfb6ec9 100644 --- a/packages/config/src/conf/default.yaml +++ b/packages/config/src/conf/default.yaml @@ -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: diff --git a/packages/config/src/conf/docker.yaml b/packages/config/src/conf/docker.yaml index 9aab78f3f..7a5a47b95 100644 --- a/packages/config/src/conf/docker.yaml +++ b/packages/config/src/conf/docker.yaml @@ -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: diff --git a/packages/core/core/src/constants.ts b/packages/core/core/src/constants.ts index 9c73ed219..ea3a2f32f 100644 --- a/packages/core/core/src/constants.ts +++ b/packages/core/core/src/constants.ts @@ -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'; diff --git a/packages/core/core/src/error-utils.ts b/packages/core/core/src/error-utils.ts index d744583b4..2f2263adf 100644 --- a/packages/core/core/src/error-utils.ts +++ b/packages/core/core/src/error-utils.ts @@ -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', diff --git a/packages/core/core/src/index.ts b/packages/core/core/src/index.ts index 60b68f87f..932d7ff64 100644 --- a/packages/core/core/src/index.ts +++ b/packages/core/core/src/index.ts @@ -20,7 +20,7 @@ export { CHARACTER_ENCODING, HEADER_TYPE, LATEST, - DEFAULT_MIN_LIMIT_PASSWORD, + DEFAULT_PASSWORD_VALIDATION, DEFAULT_USER, USERS, } from './constants'; diff --git a/packages/core/core/src/validation-utils.ts b/packages/core/core/src/validation-utils.ts index 5e3282788..127406859 100644 --- a/packages/core/core/src/validation-utils.ts +++ b/packages/core/core/src/validation-utils.ts @@ -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; +} diff --git a/packages/core/core/test/validation-utilts.spec.ts b/packages/core/core/test/validation-utilts.spec.ts index af86d2dde..0ea0faec1 100644 --- a/packages/core/core/test/validation-utilts.spec.ts +++ b/packages/core/core/test/validation-utilts.spec.ts @@ -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 { + expect(validatePassword('12')).toBeFalsy(); + }); + + test('should validate password according the length and default config', () => { + expect(validatePassword('1235678910')).toBeTruthy(); + }); +}); diff --git a/packages/core/types/src/configuration.ts b/packages/core/types/src/configuration.ts index 05b268dbe..3737a33d0 100644 --- a/packages/core/types/src/configuration.ts +++ b/packages/core/types/src/configuration.ts @@ -233,6 +233,7 @@ export type ServerSettingsConf = { * acme-XXXXXX */ pluginPrefix?: string; + passwordValidationRegex?: RegExp; }; /** diff --git a/packages/server/fastify/src/endpoints/user.ts b/packages/server/fastify/src/endpoints/user.ts index 7b05e46e1..01c02b0dc 100644 --- a/packages/server/fastify/src/endpoints/user.ts +++ b/packages/server/fastify/src/endpoints/user.ts @@ -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; diff --git a/packages/server/fastify/src/routes/web/api/login.ts b/packages/server/fastify/src/routes/web/api/login.ts index 33ff62279..24e318826 100644 --- a/packages/server/fastify/src/routes/web/api/login.ts +++ b/packages/server/fastify/src/routes/web/api/login.ts @@ -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, diff --git a/packages/store/src/local-storage.ts b/packages/store/src/local-storage.ts index 68c9e0ec6..1a13c0a5f 100644 --- a/packages/store/src/local-storage.ts +++ b/packages/store/src/local-storage.ts @@ -75,7 +75,7 @@ class LocalStorage { (plugin): IPluginStorage => { return plugin.getPackageStorage; }, - this.config?.server?.pluginPrefix + this.config?.serverSettings?.pluginPrefix ); if (plugins.length > 1) { diff --git a/packages/store/src/storage.ts b/packages/store/src/storage.ts index 789b295e5..51819d1e5 100644 --- a/packages/store/src/storage.ts +++ b/packages/store/src/storage.ts @@ -677,7 +677,7 @@ class Storage { (plugin) => { return plugin.filterMetadata; }, - this.config?.server?.pluginPrefix + this.config?.serverSettings?.pluginPrefix ); debug('filters available %o', this.filters); } diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index db89562be..44aa5663a 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, { coverageThreshold: { global: { // FIXME: increase to 90 - lines: 61, + lines: 60, }, }, }); diff --git a/packages/utils/src/auth-utils.ts b/packages/utils/src/auth-utils.ts index acd461e41..cd4bbac6e 100644 --- a/packages/utils/src/auth-utils.ts +++ b/packages/utils/src/auth-utils.ts @@ -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; diff --git a/packages/utils/test/auth-utils.spec.ts b/packages/utils/test/auth-utils.spec.ts index ca7ffd211..fcd997f3f 100644 --- a/packages/utils/test/auth-utils.spec.ts +++ b/packages/utils/test/auth-utils.spec.ts @@ -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'); diff --git a/packages/web/src/api/user.ts b/packages/web/src/api/user.ts index 6d799379b..2f391dcc9 100644 --- a/packages/web/src/api/user.ts +++ b/packages/web/src/api/user.ts @@ -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, diff --git a/packages/web/src/middleware/render-web.ts b/packages/web/src/middleware/render-web.ts index dc0c25859..93d494b0c 100644 --- a/packages/web/src/middleware/render-web.ts +++ b/packages/web/src/middleware/render-web.ts @@ -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(