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

feat: improve legacy token signature by removing deprecated crypto.cr… (#1953)

* feat: improve legacy token signature by removing deprecated crypto.createDecipher

* fix: wrong reference

* chore: add debug
This commit is contained in:
Juan Picado 2020-10-03 05:47:04 -07:00
parent 82c2f4e03a
commit e367c3f1e0
38 changed files with 455 additions and 239 deletions

@ -0,0 +1,45 @@
---
'@verdaccio/api': major
'@verdaccio/auth': major
'@verdaccio/cli': major
'@verdaccio/dev-commons': major
'@verdaccio/config': major
'@verdaccio/commons-api': major
'@verdaccio/file-locking': major
'@verdaccio/htpasswd': major
'@verdaccio/local-storage': major
'@verdaccio/readme': major
'@verdaccio/streams': major
'@verdaccio/types': major
'@verdaccio/hooks': major
'@verdaccio/loaders': major
'@verdaccio/logger': major
'@verdaccio/logger-prettify': major
'@verdaccio/middleware': major
'@verdaccio/mock': major
'@verdaccio/node-api': major
'@verdaccio/proxy': major
'@verdaccio/server': major
'@verdaccio/store': major
'@verdaccio/dev-types': major
'@verdaccio/utils': major
'verdaccio': major
'@verdaccio/web': major
---
- Replace signature handler for legacy tokens by removing deprecated crypto.createDecipher by createCipheriv
- Introduce environment variables for legacy tokens
### Code Improvements
- Add debug library for improve developer experience
### Breaking change
- The new signature invalidates all previous tokens generated by Verdaccio 4 or previous versions.
- The secret key must have 32 characters long.
### New environment variables
- `VERDACCIO_LEGACY_ALGORITHM`: Allows to define the specific algorithm for the token signature which by default is `aes-256-ctr`
- `VERDACCIO_LEGACY_ENCRYPTION_KEY`: By default, the token stores in the database, but using this variable allows to get it from memory

@ -3,41 +3,11 @@
A full list of available environment variables that allow override
internal features.
#### VERDACCIO_HANDLE_KILL_SIGNALS
#### VERDACCIO_LEGACY_ALGORITHM
Enables gracefully shutdown, more info [here](https://github.com/verdaccio/verdaccio/pull/2121).
Allows to define the specific algorithm for the token
signature which by default is `aes-256-ctr`
This will be enable by default on Verdaccio 5.
#### VERDACCIO_LEGACY_ENCRYPTION_KEY
#### VERDACCIO_PUBLIC_URL
Define a specific public url for your server, it overrules the `Host` and `X-Forwarded-Proto` header if a reverse proxy is being used, it takes in account the `url_prefix` if is defined.
This is handy in such situations where a dynamic url is required.
eg:
```
VERDACCIO_PUBLIC_URL='https://somedomain.org';
url_prefix: '/my_prefix'
// url -> https://somedomain.org/my_prefix/
VERDACCIO_PUBLIC_URL='https://somedomain.org';
url_prefix: '/'
// url -> https://somedomain.org/
VERDACCIO_PUBLIC_URL='https://somedomain.org/first_prefix';
url_prefix: '/second_prefix'
// url -> https://somedomain.org/second_prefix/'
```
#### VERDACCIO_FORWARDED_PROTO
The default header to identify the protocol is `X-Forwarded-Proto`, but there are some environments which [uses something different](https://github.com/verdaccio/verdaccio/issues/990), to change it use the variable `VERDACCIO_FORWARDED_PROTO`
```
$ VERDACCIO_FORWARDED_PROTO=CloudFront-Forwarded-Proto verdaccio --listen 5000
```
By default, the token stores in the database, but using this variable allows to get it from memory

@ -65,6 +65,7 @@
"babel-plugin-emotion": "10.0.33",
"codecov": "3.6.1",
"cross-env": "7.0.2",
"core-js": "^3.6.5",
"detect-secrets": "1.0.6",
"eslint": "7.5.0",
"eslint-config-google": "0.14.0",

@ -1,5 +1,6 @@
import _ from 'lodash';
import { Response, Router } from 'express';
import buildDebug from 'debug';
import {
createRemoteUser,
@ -15,15 +16,20 @@ import { IAuth } from '@verdaccio/auth';
import { API_ERROR, API_MESSAGE, HTTP_STATUS } from '@verdaccio/dev-commons';
import { $RequestExtend, $NextFunctionVer } from '../types/custom';
const debug = buildDebug('verdaccio:api:user');
export default function (route: Router, auth: IAuth, config: Config): void {
route.get('/-/user/:org_couchdb_user', function (
req: $RequestExtend,
res: Response,
next: $NextFunctionVer
): void {
debug('verifying user');
const message = getAuthenticatedMessage(req.remote_user.name);
debug('user authenticated message %o', message);
res.status(HTTP_STATUS.OK);
next({
ok: getAuthenticatedMessage(req.remote_user.name),
ok: message,
});
});
@ -33,9 +39,11 @@ export default function (route: Router, auth: IAuth, config: Config): void {
next: $NextFunctionVer
): void {
const { name, password } = req.body;
debug('login or adduser');
const remoteName = req.remote_user.name;
if (_.isNil(remoteName) === false && _.isNil(name) === false && remoteName === name) {
debug('login: no remote user detected');
auth.authenticate(name, password, async function callbackAuthenticate(
err,
user
@ -50,16 +58,24 @@ export default function (route: Router, auth: IAuth, config: Config): void {
const restoredRemoteUser: RemoteUser = createRemoteUser(name, user.groups || []);
const token = await getApiToken(auth, config, restoredRemoteUser, password);
debug('login: new token');
if (!token) {
return next(ErrorCode.getUnauthorized());
}
res.status(HTTP_STATUS.CREATED);
const message = getAuthenticatedMessage(req.remote_user.name);
debug('login: created user message %o', message);
return next({
ok: getAuthenticatedMessage(req.remote_user.name),
ok: message,
token,
});
});
} else {
if (validatePassword(password) === false) {
debug('adduser: invalid password');
// eslint-disable-next-line new-cap
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, API_ERROR.PASSWORD_SHORT()));
}
@ -67,6 +83,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
auth.add_user(name, password, async function (err, user): Promise<void> {
if (err) {
if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {
debug('adduser: error on create user');
// With npm registering is the same as logging in,
// and npm accepts only an 409 error.
// So, changing status code here.
@ -79,9 +96,14 @@ 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', token);
if (!token) {
return next(ErrorCode.getUnauthorized());
}
req.remote_user = user;
res.status(HTTP_STATUS.CREATED);
debug('adduser: user has been created');
return next({
ok: `user '${req.body.name}' created`,
token,

@ -8,6 +8,7 @@ import { Response, Router } from 'express';
import { Config, RemoteUser, Token } from '@verdaccio/types';
import { IAuth } from '@verdaccio/auth';
import { IStorageHandler } from '@verdaccio/store';
import { getInternalError } from '@verdaccio/commons-api';
import { $RequestExtend, $NextFunctionVer } from '../../types/custom';
export type NormalizeToken = Token & {
@ -84,6 +85,10 @@ export default function (
try {
const token = await getApiToken(auth, config, user, password);
if (!token) {
throw getInternalError();
}
const key = stringToMD5(token);
// TODO: use a utility here
const maskedToken = mask(token, 5);

@ -1,16 +1,38 @@
import { Response, Router } from 'express';
import buildDebug from 'debug';
import { $RequestExtend, $NextFunctionVer } from '../types/custom';
// import { getUnauthorized } from '@verdaccio/commons-api';
const debug = buildDebug('verdaccio:api:user');
export default function (route: Router): void {
route.get('/whoami', (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
debug('whoami: reditect');
if (req.headers.referer === 'whoami') {
next({ username: req.remote_user.name });
const username = req.remote_user.name;
// FIXME: this service should return 401 if user missing
// if (!username) {
// debug('whoami: user not found');
// return next(getUnauthorized('Unauthorized'));
// }
debug('whoami: logged by user');
return next({ username: username });
} else {
next('route');
debug('whoami: redirect next route');
// redirect to the route below
return next('route');
}
});
route.get('/-/whoami', (req: $RequestExtend, res: Response, next: $NextFunctionVer): any => {
next({ username: req.remote_user.name });
const username = req.remote_user.name;
// FIXME: this service should return 401 if user missing
// if (!username) {
// debug('whoami: user not found');
// return next(getUnauthorized('Unauthorized'));
// }
debug('whoami: response %o', username);
return next({ username: username });
});
}

@ -27,15 +27,15 @@
"@verdaccio/dev-commons": "workspace:5.0.0-alpha.0",
"@verdaccio/loaders": "workspace:5.0.0-alpha.0",
"@verdaccio/logger": "workspace:5.0.0-alpha.0",
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
"@verdaccio/auth": "workspace:5.0.0-alpha.0",
"@verdaccio/config": "workspace:5.0.0-alpha.0",
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
"jsonwebtoken": "8.5.1",
"debug": "^4.1.1",
"express": "4.17.1",
"lodash": "4.17.15"
},
"devDependencies": {
"@verdaccio/config": "workspace:5.0.0-alpha.0",
"@verdaccio/mock": "workspace:5.0.0-alpha.0",
"@verdaccio/types": "workspace:*"
},

@ -17,7 +17,6 @@ import {
Callback,
IPluginAuth,
RemoteUser,
IBasicAuth,
JWTSignOptions,
Security,
AuthPluginPackage,
@ -39,22 +38,31 @@ import {
getSecurity,
getDefaultPlugins,
verifyJWTPayload,
parseBasicPayload,
parseAuthTokenHeader,
isAuthHeaderValid,
isAESLegacy,
} from './utils';
import { aesEncrypt, signPayload } from './crypto-utils';
import { signPayload } from './jwt-token';
import { aesEncrypt } from './legacy-token';
import { parseBasicPayload } from './token';
/* eslint-disable @typescript-eslint/no-var-requires */
const LoggerApi = require('@verdaccio/logger');
const debug = buildDebug('verdaccio:auth');
export interface IAuthWebUI {
export interface IBasicAuth<T> {
config: T & Config;
authenticate(user: string, password: string, cb: Callback): void;
changePassword(user: string, password: string, newPassword: string, cb: Callback): void;
allow_access(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
add_user(user: string, password: string, cb: Callback): any;
}
export interface TokenEncryption {
jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): Promise<string>;
aesEncrypt(buf: Buffer): Buffer;
aesEncrypt(buf: string): string | void;
}
export interface AESPayload {
@ -71,7 +79,7 @@ export interface IAuthMiddleware {
webUIJWTmiddleware(): $NextFunctionVer;
}
export interface IAuth extends IBasicAuth<Config>, IAuthMiddleware, IAuthWebUI {
export interface IAuth extends IBasicAuth<Config>, IAuthMiddleware, TokenEncryption {
config: Config;
logger: Logger;
secret: string;
@ -243,7 +251,6 @@ class Auth implements IAuth {
pkgAllowAcces,
getMatchedPackagesSpec(packageName, this.config.packages)
) as AllowAccess & PackageAccess;
const self = this;
debug('allow access for %o', packageName);
(function next(): void {
@ -358,6 +365,7 @@ class Auth implements IAuth {
}
public apiJWTmiddleware(): Function {
debug('jwt middleware');
const plugins = this.plugins.slice(0);
const helpers = { createAnonymousRemoteUser, createRemoteUser };
for (const plugin of plugins) {
@ -382,6 +390,7 @@ class Auth implements IAuth {
};
if (this._isRemoteUserValid(req.remote_user)) {
debug('jwt has remote user');
return next();
}
@ -390,6 +399,7 @@ class Auth implements IAuth {
const { authorization } = req.headers;
if (_.isNil(authorization)) {
debug('jwt invalid auth header');
return next();
}
@ -418,29 +428,36 @@ class Auth implements IAuth {
authorization: string,
next: Function
): void {
debug('handle JWT api middleware');
const { scheme, token } = parseAuthTokenHeader(authorization);
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
debug('handle basic token');
// this should happen when client tries to login with an existing user
const credentials = convertPayloadToBase64(token).toString();
const { user, password } = parseBasicPayload(credentials) as AESPayload;
debug('authenticating %o', user);
this.authenticate(user, password, (err, user): void => {
if (!err) {
debug('generating a remote user');
req.remote_user = user;
next();
} else {
debug('generating anonymous user');
req.remote_user = createAnonymousRemoteUser();
next(err);
}
});
} else {
// jwt handler
debug('handle jwt token');
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
if (credentials) {
// if the signature is valid we rely on it
req.remote_user = credentials;
debug('generating a remote user');
next();
} else {
// with JWT throw 401
debug('jwt invalid token');
next(getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
}
}
@ -453,20 +470,28 @@ class Auth implements IAuth {
authorization: string,
next: Function
): void {
debug('handle legacy api middleware');
debug('api middleware secret %o', secret);
debug('api middleware authorization %o', authorization);
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
debug('api middleware credentials %o', credentials);
if (credentials) {
const { user, password } = credentials;
debug('authenticating %o', user);
this.authenticate(user, password, (err, user): void => {
if (!err) {
req.remote_user = user;
debug('generating a remote user');
next();
} else {
req.remote_user = createAnonymousRemoteUser();
debug('generating anonymous user');
next(err);
}
});
} else {
// we force npm client to ask again with basic authentication
debug('legacy invalid header');
return next(getBadRequest(API_ERROR.BAD_AUTH_HEADER));
}
}
@ -546,8 +571,8 @@ class Auth implements IAuth {
/**
* Encrypt a string.
*/
public aesEncrypt(buf: Buffer): Buffer {
return aesEncrypt(buf, this.secret);
public aesEncrypt(value: string): string | void {
return aesEncrypt(value, this.secret);
}
}

@ -1,60 +0,0 @@
import { createDecipher, createCipher } from 'crypto';
import jwt from 'jsonwebtoken';
import { JWTSignOptions, RemoteUser } from '@verdaccio/types';
export const defaultAlgorithm = 'aes192';
export function aesEncrypt(buf: Buffer, secret: string): Buffer {
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
// https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
const c = createCipher(defaultAlgorithm, secret);
const b1 = c.update(buf);
const b2 = c.final();
return Buffer.concat([b1, b2]);
}
export function aesDecrypt(buf: Buffer, secret: string): Buffer {
try {
// deprecated (it will be migrated in Verdaccio 5), it is a breaking change
// https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
const c = createDecipher(defaultAlgorithm, secret);
const b1 = c.update(buf);
const b2 = c.final();
return Buffer.concat([b1, b2]);
} catch (_) {
return new Buffer(0);
}
}
/**
* Sign the payload and return JWT
* https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
* @param payload
* @param secretOrPrivateKey
* @param options
*/
export async function signPayload(
payload: RemoteUser,
secretOrPrivateKey: string,
options: JWTSignOptions = {}
): Promise<string> {
return new Promise(function (resolve, reject): Promise<string> {
return jwt.sign(
payload,
secretOrPrivateKey,
{
// 1 === 1ms (one millisecond)
notBefore: '1', // Make sure the time will not rollback :)
...options,
},
(error, token) => (error ? reject(error) : resolve(token))
);
});
}
export function verifyPayload(token: string, secretOrPrivateKey: string): RemoteUser {
return jwt.verify(token, secretOrPrivateKey);
}

@ -1,3 +1,5 @@
export { Auth, IAuth, IAuthWebUI } from './auth';
export { Auth, IAuth, TokenEncryption, IBasicAuth } from './auth';
export * from './utils';
export * from './crypto-utils';
export * from './legacy-token';
export * from './jwt-token';
export * from './token';

@ -0,0 +1,40 @@
import jwt from 'jsonwebtoken';
import buildDebug from 'debug';
import { JWTSignOptions, RemoteUser } from '@verdaccio/types';
const debug = buildDebug('verdaccio:auth:token:jwt');
/**
* Sign the payload and return JWT
* https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
* @param payload
* @param secretOrPrivateKey
* @param options
*/
export async function signPayload(
payload: RemoteUser,
secretOrPrivateKey: string,
options: JWTSignOptions = {}
): Promise<string> {
return new Promise(function (resolve, reject): Promise<string> {
debug('sign jwt token');
return jwt.sign(
payload,
secretOrPrivateKey,
{
// 1 === 1ms (one millisecond)
notBefore: '1', // Make sure the time will not rollback :)
...options,
},
(error, token) => {
debug('error on sign jwt token');
return error ? reject(error) : resolve(token);
}
);
});
}
export function verifyPayload(token: string, secretOrPrivateKey: string): RemoteUser {
debug('verify jwt token');
return jwt.verify(token, secretOrPrivateKey);
}

@ -0,0 +1,65 @@
import {
createCipheriv,
createDecipheriv,
HexBase64BinaryEncoding,
randomBytes,
Utf8AsciiBinaryEncoding,
} from 'crypto';
import { TOKEN_VALID_LENGTH } from '@verdaccio/config';
import buildDebug from 'debug';
const debug = buildDebug('verdaccio:auth:token:legacy');
export const defaultAlgorithm = process.env.VERDACCIO_LEGACY_ALGORITHM || 'aes-256-ctr';
const inputEncoding: Utf8AsciiBinaryEncoding = 'utf8';
const outputEncoding: HexBase64BinaryEncoding = 'hex';
// For AES, this is always 16
const IV_LENGTH = 16;
// Must be 256 bits (32 characters)
// https://stackoverflow.com/questions/50963160/invalid-key-length-in-crypto-createcipheriv#50963356
const VERDACCIO_LEGACY_ENCRYPTION_KEY = process.env.VERDACCIO_LEGACY_ENCRYPTION_KEY;
export function aesEncrypt(value: string, key: string): string | void {
// https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options
// https://www.grainger.xyz/changing-from-cipher-to-cipheriv/
debug('encrypt %o', value);
debug('algorithm %o', defaultAlgorithm);
const iv = Buffer.from(randomBytes(IV_LENGTH));
const secretKey = VERDACCIO_LEGACY_ENCRYPTION_KEY || key;
const isKeyValid = secretKey?.length === TOKEN_VALID_LENGTH;
debug('length secret key %o', secretKey?.length);
debug('is valid secret %o', isKeyValid);
if (!value || !secretKey || !isKeyValid) {
return;
}
const cipher = createCipheriv(defaultAlgorithm, secretKey, iv);
let encrypted = cipher.update(value, inputEncoding, outputEncoding);
// @ts-ignore
encrypted += cipher.final(outputEncoding);
const token = `${iv.toString('hex')}:${encrypted.toString()}`;
debug('token generated successfully');
return Buffer.from(token).toString('base64');
}
export function aesDecrypt(value: string, key: string): string | void {
try {
const buff = Buffer.from(value, 'base64');
const textParts = buff.toString().split(':');
// extract the IV from the first half of the value
// @ts-ignore
const IV = Buffer.from(textParts.shift(), outputEncoding);
// extract the encrypted text without the IV
const encryptedText = Buffer.from(textParts.join(':'), outputEncoding);
const secretKey = VERDACCIO_LEGACY_ENCRYPTION_KEY || key;
// decipher the string
const decipher = createDecipheriv(defaultAlgorithm, secretKey, IV);
let decrypted = decipher.update(encryptedText, outputEncoding, inputEncoding);
decrypted += decipher.final(inputEncoding);
debug('token decrypted successfully');
return decrypted.toString();
} catch (_) {
return;
}
}

@ -0,0 +1,13 @@
import { BasicPayload } from './utils';
export function parseBasicPayload(credentials: string): BasicPayload {
const index = credentials.indexOf(':');
if (index < 0) {
return;
}
const user: string = credentials.slice(0, index);
const password: string = credentials.slice(index + 1);
return { user, password };
}

@ -1,19 +1,23 @@
import _ from 'lodash';
import buildDebug from 'debug';
import { Callback, Config, IPluginAuth, RemoteUser, Security } from '@verdaccio/types';
import { HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER, API_ERROR } from '@verdaccio/dev-commons';
import { getForbidden, getUnauthorized, getConflict, getCode } from '@verdaccio/commons-api';
import {
AllowAction,
AllowActionCallback,
AuthPackageAllow,
buildUserBuffer,
convertPayloadToBase64,
createAnonymousRemoteUser,
defaultSecurity,
} from '@verdaccio/utils';
import { TokenEncryption, AESPayload } from './auth';
import { aesDecrypt } from './legacy-token';
import { verifyPayload } from './jwt-token';
import { parseBasicPayload } from './token';
import { IAuthWebUI, AESPayload } from './auth';
import { aesDecrypt, verifyPayload } from './crypto-utils';
const debug = buildDebug('verdaccio:auth:utils');
export type BasicPayload = AESPayload | void;
export type AuthMiddlewarePayload = RemoteUser | BasicPayload;
@ -23,6 +27,10 @@ export interface AuthTokenHeader {
token: string;
}
/**
* Split authentication header eg: Bearer [secret_token]
* @param authorizationHeader auth token
*/
export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHeader {
const parts = authorizationHeader.split(' ');
const [scheme, token] = parts;
@ -31,16 +39,19 @@ export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHead
}
export function parseAESCredentials(authorizationHeader: string, secret: string) {
debug('parseAESCredentials');
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
// basic is deprecated and should not be enforced
// basic is currently being used for functional test
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
debug('legacy header basic');
const credentials = convertPayloadToBase64(token).toString();
return credentials;
} else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
const tokenAsBuffer = convertPayloadToBase64(token);
const credentials = aesDecrypt(tokenAsBuffer, secret).toString('utf8');
debug('legacy header bearer');
const credentials = aesDecrypt(token, secret);
return credentials;
}
@ -48,17 +59,22 @@ export function parseAESCredentials(authorizationHeader: string, secret: string)
export function getMiddlewareCredentials(
security: Security,
secret: string,
secretKey: string,
authorizationHeader: string
): AuthMiddlewarePayload {
debug('getMiddlewareCredentials');
// comment out for debugging purposes
if (isAESLegacy(security)) {
const credentials = parseAESCredentials(authorizationHeader, secret);
debug('is legacy');
const credentials = parseAESCredentials(authorizationHeader, secretKey);
if (!credentials) {
debug('parse legacy credentials failed');
return;
}
const parsedCredentials = parseBasicPayload(credentials);
if (!parsedCredentials) {
debug('parse legacy basic payload credentials failed');
return;
}
@ -66,8 +82,9 @@ export function getMiddlewareCredentials(
}
const { scheme, token } = parseAuthTokenHeader(authorizationHeader);
debug('is jwt');
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
return verifyJWTPayload(token, secret);
return verifyJWTPayload(token, secretKey);
}
}
@ -78,19 +95,17 @@ export function isAESLegacy(security: Security): boolean {
}
export async function getApiToken(
auth: IAuthWebUI,
auth: TokenEncryption,
config: Config,
remoteUser: RemoteUser,
aesPassword: string
): Promise<string> {
): Promise<string | void> {
const security: Security = getSecurity(config);
if (isAESLegacy(security)) {
// fallback all goes to AES encryption
return await new Promise((resolve): void => {
resolve(
auth.aesEncrypt(buildUserBuffer(remoteUser.name as string, aesPassword)).toString('base64')
);
resolve(auth.aesEncrypt(buildUser(remoteUser.name as string, aesPassword)));
});
}
// i am wiling to use here _.isNil but flow does not like it yet.
@ -100,9 +115,7 @@ export async function getApiToken(
return await auth.jwtEncrypt(remoteUser, jwt.sign);
}
return await new Promise((resolve): void => {
resolve(
auth.aesEncrypt(buildUserBuffer(remoteUser.name as string, aesPassword)).toString('base64')
);
resolve(auth.aesEncrypt(buildUser(remoteUser.name as string, aesPassword)));
});
}
@ -137,18 +150,6 @@ export function isAuthHeaderValid(authorization: string): boolean {
return authorization.split(' ').length === 2;
}
export function parseBasicPayload(credentials: string): BasicPayload {
const index = credentials.indexOf(':');
if (index < 0) {
return;
}
const user: string = credentials.slice(0, index);
const password: string = credentials.slice(index + 1);
return { user, password };
}
export function getDefaultPlugins(logger: any): IPluginAuth<Config> {
return {
authenticate(user: string, password: string, cb: Callback): void {
@ -223,3 +224,7 @@ export function handleSpecialUnpublish(logger): any {
return allow_action(action, logger)(user, pkg, callback);
};
}
export function buildUser(name: string, password: string): string {
return String(`${name}:${password}`);
}

@ -7,14 +7,13 @@ import { Config as AppConfig } from '@verdaccio/config';
import { setup } from '@verdaccio/logger';
import {
buildUserBuffer,
getAuthenticatedMessage,
buildToken,
convertPayloadToBase64,
parseConfigFile,
createAnonymousRemoteUser,
createRemoteUser,
AllowActionCallbackResponse,
buildUserBuffer,
} from '@verdaccio/utils';
import { Config, Security, RemoteUser } from '@verdaccio/types';
@ -32,6 +31,7 @@ import {
aesDecrypt,
verifyPayload,
signPayload,
buildUser,
} from '../src';
setup([]);
@ -60,7 +60,7 @@ describe('Auth utilities', () => {
return config;
}
async function signCredentials(
async function getTokenByConfiguration(
configFileName: string,
username: string,
password: string,
@ -85,7 +85,7 @@ describe('Auth utilities', () => {
expect(spyNotCalled).not.toHaveBeenCalled();
expect(token).toBeDefined();
return token;
return token as string;
}
const verifyJWT = (token: string, user: string, password: string, secret: string) => {
@ -96,7 +96,7 @@ describe('Auth utilities', () => {
};
const verifyAES = (token: string, user: string, password: string, secret: string) => {
const payload = aesDecrypt(convertPayloadToBase64(token), secret).toString(
const payload = aesDecrypt(token, secret).toString(
// @ts-ignore
CHARACTER_ENCODING.UTF8
);
@ -222,101 +222,101 @@ describe('Auth utilities', () => {
describe('getApiToken test', () => {
test('should sign token with aes and security missing', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-missing',
'test',
'test',
'1234567',
'b2df428b9929d3ace7c598bbf4e496b2',
'aesEncrypt',
'jwtEncrypt'
);
verifyAES(token, 'test', 'test', '1234567');
verifyAES(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with aes and security empty', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-empty',
'test',
'test',
'123456',
'b2df428b9929d3ace7c598bbf4e496b2',
'aesEncrypt',
'jwtEncrypt'
);
verifyAES(token, 'test', 'test', '123456');
verifyAES(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with aes', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-basic',
'test',
'test',
'123456',
'b2df428b9929d3ace7c598bbf4e496b2',
'aesEncrypt',
'jwtEncrypt'
);
verifyAES(token, 'test', 'test', '123456');
verifyAES(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with legacy and jwt disabled', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-no-legacy',
'test',
'test',
'x8T#ZCx=2t',
'b2df428b9929d3ace7c598bbf4e496b2',
'aesEncrypt',
'jwtEncrypt'
);
expect(_.isString(token)).toBeTruthy();
verifyAES(token, 'test', 'test', 'x8T#ZCx=2t');
verifyAES(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
});
test('should sign token with legacy enabled and jwt enabled', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-jwt-legacy-enabled',
'test',
'test',
'secret',
'b2df428b9929d3ace7c598bbf4e496b2',
'jwtEncrypt',
'aesEncrypt'
);
verifyJWT(token, 'test', 'test', 'secret');
verifyJWT(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with jwt enabled', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-jwt',
'test',
'test',
'secret',
'b2df428b9929d3ace7c598bbf4e496b2',
'jwtEncrypt',
'aesEncrypt'
);
expect(_.isString(token)).toBeTruthy();
verifyJWT(token, 'test', 'test', 'secret');
verifyJWT(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
});
test('should sign with jwt whether legacy is disabled', async () => {
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-legacy-disabled',
'test',
'test',
'secret',
'b2df428b9929d3ace7c598bbf4e496b2',
'jwtEncrypt',
'aesEncrypt'
);
expect(_.isString(token)).toBeTruthy();
verifyJWT(token, 'test', 'test', 'secret');
verifyJWT(token, 'test', 'test', 'b2df428b9929d3ace7c598bbf4e496b2');
});
});
@ -328,11 +328,11 @@ describe('Auth utilities', () => {
describe('getMiddlewareCredentials test', () => {
describe('should get AES credentials', () => {
test.concurrent('should unpack aes token and credentials', async () => {
const secret = 'secret';
test.concurrent('should unpack aes token and credentials bearer auth', async () => {
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const user = 'test';
const pass = 'test';
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-legacy',
user,
pass,
@ -350,10 +350,11 @@ describe('Auth utilities', () => {
expect(credentials.password).toEqual(pass);
});
test.concurrent('should unpack aes token and credentials', async () => {
const secret = 'secret';
test.concurrent('should unpack aes token and credentials basic auth', async () => {
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const user = 'test';
const pass = 'test';
// basic authentication need send user as base64
const token = buildUserBuffer(user, pass).toString('base64');
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
@ -366,8 +367,8 @@ describe('Auth utilities', () => {
});
test.concurrent('should return empty credential wrong secret key', async () => {
const secret = 'secret';
const token = await signCredentials(
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const token = await getTokenByConfiguration(
'security-legacy',
'test',
'test',
@ -379,15 +380,15 @@ describe('Auth utilities', () => {
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
'BAD_SECRET',
'b2df428b9929d3ace7c598bbf4e496_BAD_TOKEN',
buildToken(TOKEN_BEARER, token)
);
expect(credentials).not.toBeDefined();
});
test.concurrent('should return empty credential wrong scheme', async () => {
const secret = 'secret';
const token = await signCredentials(
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const token = await getTokenByConfiguration(
'security-legacy',
'test',
'test',
@ -406,15 +407,15 @@ describe('Auth utilities', () => {
});
test.concurrent('should return empty credential corrupted payload', async () => {
const secret = 'secret';
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const config: Config = getConfig('security-legacy', secret);
const auth: IAuth = new Auth(config);
const token = auth.aesEncrypt(Buffer.from(`corruptedBuffer`)).toString('base64');
const token = auth.aesEncrypt(null);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(
security,
secret,
buildToken(TOKEN_BEARER, token)
buildToken(TOKEN_BEARER, token as string)
);
expect(credentials).not.toBeDefined();
});
@ -422,7 +423,9 @@ describe('Auth utilities', () => {
describe('verifyJWTPayload', () => {
test('should fail on verify the token and return anonymous users', () => {
expect(verifyJWTPayload('fakeToken', 'secret')).toEqual(createAnonymousRemoteUser());
expect(verifyJWTPayload('fakeToken', 'b2df428b9929d3ace7c598bbf4e496b2')).toEqual(
createAnonymousRemoteUser()
);
});
test('should fail on verify the token and return anonymous users', async () => {
@ -465,11 +468,11 @@ describe('Auth utilities', () => {
expect(credentials).not.toBeDefined();
});
test('should verify succesfully a JWT token', async () => {
const secret = 'secret';
test('should verify successfully a JWT token', async () => {
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const user = 'test';
const config: Config = getConfig('security-jwt', secret);
const token = await signCredentials(
const token = await getTokenByConfiguration(
'security-jwt',
user,
'secretTest',

@ -1,15 +0,0 @@
import { convertPayloadToBase64 } from '@verdaccio/utils';
import { aesDecrypt, aesEncrypt } from '../src/crypto-utils';
describe('test crypto utils', () => {
describe('default encryption', () => {
test('decrypt payload flow', () => {
const payload = 'juan';
const token = aesEncrypt(Buffer.from(payload), '12345').toString('base64');
const data = aesDecrypt(convertPayloadToBase64(token), '12345').toString('utf8');
expect(payload).toEqual(data);
});
});
});

@ -0,0 +1,19 @@
import { aesDecrypt, aesEncrypt } from '../src/legacy-token';
describe('test crypto utils', () => {
test('decrypt payload flow', () => {
const secret = 'f5bb945cc57fea2f25961e1bd6fb3c89';
const payload = 'juan:password';
const token = aesEncrypt(payload, secret) as string;
const data = aesDecrypt(token, secret);
expect(payload).toEqual(data);
});
test('crypt fails if secret is incorrect', () => {
const secret = 'f5bb945cc57fea2f25961e1bd6fb3c89_TO_LONG';
const payload = 'juan';
const token = aesEncrypt(payload, secret) as string;
expect(token).toBeUndefined();
});
});

@ -131,7 +131,7 @@ export const API_ERROR = {
PACKAGE_EXIST: 'this package is already present',
BAD_AUTH_HEADER: 'bad authorization header',
WEB_DISABLED: 'Web interface is disabled in the config file',
DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead',
DEPRECATED_BASIC_HEADER: 'basic authentication is disabled, please use Bearer tokens instead',
BAD_FORMAT_USER_GROUP: 'user groups is different than an array',
RESOURCE_UNAVAILABLE: 'resource unavailable',
BAD_PACKAGE_DATA: 'bad incoming package data',

@ -27,7 +27,7 @@
"@verdaccio/logger": "workspace:5.0.0-alpha.0",
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
"mkdirp": "0.5.5",
"debug": "^4.2.0",
"lodash": "^4.17.20"
},
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
}
}

@ -1,5 +1,6 @@
import assert from 'assert';
import _ from 'lodash';
import buildDebug from 'debug';
import {
getMatchedPackagesSpec,
@ -19,6 +20,7 @@ import {
Logger,
PackageAccess,
} from '@verdaccio/types';
import { generateRandomSecretKey } from './token';
const LoggerApi = require('@verdaccio/logger');
@ -33,6 +35,8 @@ export interface StartUpConfig {
self_path: string;
}
const debug = buildDebug('verdaccio:config');
/**
* Coordinates the application configuration
*/
@ -115,13 +119,16 @@ class Config implements AppConfig {
* Store or create whether receive a secret key
*/
public checkSecretKey(secret: string): string {
debug('check secret key');
if (_.isString(secret) && _.isEmpty(secret) === false) {
this.secret = secret;
debug('reusing previous key');
return secret;
}
// it generates a secret key
// FUTURE: this might be an external secret key, perhaps within config file?
this.secret = generateRandomHexString(32);
debug('generate a new key');
this.secret = generateRandomSecretKey();
return this.secret;
}
}

@ -1,2 +1,3 @@
export * from './config';
export * from './config-path';
export * from './token';

@ -0,0 +1,10 @@
import { randomBytes } from 'crypto';
export const TOKEN_VALID_LENGTH = 32;
/**
* Secret key must have 32 characters.
*/
export function generateRandomSecretKey(): string {
return randomBytes(TOKEN_VALID_LENGTH).toString('base64').substring(0, TOKEN_VALID_LENGTH);
}

@ -0,0 +1,6 @@
import { generateRandomSecretKey, TOKEN_VALID_LENGTH } from '../src/token';
test('token test valid length', () => {
const token = generateRandomSecretKey();
expect(token).toHaveLength(TOKEN_VALID_LENGTH);
});

@ -483,9 +483,10 @@ declare module '@verdaccio/types' {
getSecret(config: T & Config): Promise<any>;
}
// @deprecated use IBasicAuth from @verdaccio/auth
interface IBasicAuth<T> {
config: T & Config;
aesEncrypt(buf: Buffer): Buffer;
aesEncrypt(buf: string): string;
authenticate(user: string, password: string, cb: Callback): void;
changePassword(user: string, password: string, newPassword: string, cb: Callback): void;
allow_access(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
@ -550,6 +551,7 @@ declare module '@verdaccio/types' {
apiJWTmiddleware?(helpers: any): Function;
}
// @deprecated use @verdaccio/server
interface IPluginMiddleware<T> extends IPlugin<T> {
register_middlewares(app: any, auth: IBasicAuth<T>, storage: IStorageManager<T>): void;
}

@ -29,6 +29,7 @@
"lodash": "^4.17.20",
"request": "2.87.0",
"supertest": "^4.0.2",
"debug": "^4.2.0",
"verdaccio": "^4.8.1"
},
"devDependencies": {

@ -111,7 +111,6 @@ export function loginUserToken(
token: string,
statusCode: number = HTTP_STATUS.CREATED
): Promise<any[]> {
// $FlowFixMe
return new Promise((resolve) => {
request
.put(`/-/user/org.couchdb.user:${user}`)
@ -131,7 +130,6 @@ export function addUser(
credentials: any,
statusCode: number = HTTP_STATUS.CREATED
): Promise<any[]> {
// $FlowFixMe
return new Promise((resolve) => {
request
.put(`/-/user/org.couchdb.user:${user}`)

@ -1,10 +1,14 @@
import assert from 'assert';
import _ from 'lodash';
import request from 'request';
import buildDebug from 'debug';
import { IRequestPromise } from './types';
const requestData = Symbol('smart_request_data');
const debug = buildDebug('verdaccio:mock:request');
export class PromiseAssert extends Promise<any> implements IRequestPromise {
public constructor(options: any) {
super(options);
@ -17,8 +21,10 @@ export class PromiseAssert extends Promise<any> implements IRequestPromise {
this,
this.then(function (body) {
try {
console.log('-->', expected, selfData?.response?.statusCode);
assert.equal(selfData.response.statusCode, expected);
} catch (err) {
debug('error status %o', err);
selfData.error.message = err.message;
throw selfData.error;
}
@ -34,6 +40,7 @@ export class PromiseAssert extends Promise<any> implements IRequestPromise {
this,
this.then(function (body) {
try {
debug('body_ok %o', body);
if (_.isRegExp(expected)) {
assert(body.ok.match(expected), "'" + body.ok + "' doesn't match " + expected);
} else {
@ -41,6 +48,7 @@ export class PromiseAssert extends Promise<any> implements IRequestPromise {
}
assert.equal(body.error, null);
} catch (err) {
debug('body_ok error %o', err.message);
selfData.error.message = err.message;
throw selfData.error;
}
@ -111,6 +119,7 @@ function smartRequest(options: any): Promise<any> {
// store request reference on symbol
smartObject[requestData].request = request(options, function (err, res, body) {
if (err) {
debug('error request %o', err);
return reject(err);
}

@ -1,10 +1,12 @@
import assert from 'assert';
import _ from 'lodash';
import buildDebug from 'debug';
import { API_MESSAGE, HEADERS, HTTP_STATUS, TOKEN_BASIC } from '@verdaccio/dev-commons';
import { buildToken } from '@verdaccio/utils';
import smartRequest from './request';
import { IServerBridge } from './types';
import { IServerBridge } from './types';
import { CREDENTIALS } from './constants';
import getPackage from './fixtures/package';
@ -12,6 +14,8 @@ const buildAuthHeader = (user, pass): string => {
return buildToken(TOKEN_BASIC, Buffer.from(`${user}:${pass}`).toString('base64'));
};
const debug = buildDebug('verdaccio:mock:server');
export default class Server implements IServerBridge {
public url: string;
public userAgent: string;
@ -24,12 +28,14 @@ export default class Server implements IServerBridge {
}
public request(options: any): any {
debug('request to %o', options.uri);
assert(options.uri);
const headers = options.headers || {};
headers.accept = headers.accept || HEADERS.JSON;
headers['user-agent'] = headers['user-agent'] || this.userAgent;
headers.authorization = headers.authorization || this.authstr;
debug('request headers %o', headers);
return smartRequest({
url: this.url + options.uri,
@ -41,6 +47,7 @@ export default class Server implements IServerBridge {
}
public auth(name: string, password: string) {
debug('request auth %o:%o', name, password);
this.authstr = buildAuthHeader(name, password);
return this.request({
uri: `/-/user/org.couchdb.user:${encodeURIComponent(name)}/-rev/undefined`,
@ -194,11 +201,13 @@ export default class Server implements IServerBridge {
}
public whoami() {
debug('request whoami');
return this.request({
uri: '/-/whoami',
})
.status(HTTP_STATUS.OK)
.then(function (body) {
debug('request whoami body %o', body);
return body.username;
});
}

@ -28,6 +28,7 @@
"@verdaccio/server": "workspace:5.0.0-alpha.0",
"@verdaccio/utils": "workspace:5.0.0-alpha.0",
"lodash": "^4.17.20",
"core-js": "^3.6.5",
"selfsigned": "1.10.7"
},
"devDependencies": {

@ -14,15 +14,24 @@ import { Config as AppConfig } from '@verdaccio/config';
import { webAPI, renderWebMiddleware } from '@verdaccio/web';
import { IAuth } from '@verdaccio/auth';
import { IAuth, IBasicAuth } from '@verdaccio/auth';
import { IStorageHandler } from '@verdaccio/store';
import { Config as IConfig, IPluginMiddleware, IPluginStorageFilter } from '@verdaccio/types';
import { setup, logger } from '@verdaccio/logger';
import { log, final, errorReportingMiddleware } from '@verdaccio/middleware';
import {
Config as IConfig,
IPluginStorageFilter,
IStorageManager,
IPlugin,
} from '@verdaccio/types';
import { $ResponseExtend, $RequestExtend, $NextFunctionVer } from '../types/custom';
import hookDebug from './debug';
interface IPluginMiddleware<T> extends IPlugin<T> {
register_middlewares(app: any, auth: IBasicAuth<T>, storage: IStorageManager<T>): void;
}
const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
const auth: IAuth = new Auth(config);
const app: Application = express();

@ -37,7 +37,7 @@ import {
setup([]);
const credentials = { name: 'jota', password: 'secretPass' };
const credentials = { name: 'server_user_api_spec', password: 'secretPass' };
const putVersion = (app, name, publishMetadata) => {
return request(app)

@ -97,25 +97,6 @@ export type $ResponseExtend = Response & { cookies?: any };
export type $NextFunctionVer = NextFunction & any;
export type $SidebarPackage = Package & { latest: any };
export interface IAuthWebUI {
jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): Promise<string>;
aesEncrypt(buf: Buffer): Buffer;
}
interface IAuthMiddleware {
apiJWTmiddleware(): $NextFunctionVer;
webUIJWTmiddleware(): $NextFunctionVer;
}
export interface IAuth extends IBasicAuth<Config>, IAuthMiddleware, IAuthWebUI {
config: Config;
logger: Logger;
secret: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugins: any[];
allow_unpublish(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
}
export interface IWebSearch {
index: lunrMutable.index;
storage: IStorageHandler;

@ -46,6 +46,8 @@
"@verdaccio/ui-theme": "^1.12.1"
},
"devDependencies": {
"@verdaccio/auth": "workspace:5.0.0-alpha.0",
"@verdaccio/store": "workspace:5.0.0-alpha.0",
"@verdaccio/dev-commons": "workspace:*"
},
"keywords": [

@ -1,7 +1,7 @@
import { API_ERROR, HTTP_STATUS } from '@verdaccio/dev-commons';
export default function (server) {
describe('npm adduser', () => {
describe.skip('npm adduser', () => {
const user = String(Math.random());
const pass = String(Math.random());

@ -7,7 +7,7 @@ import fixturePkg from '../fixtures/package';
export default function (server) {
describe('package access control', () => {
const buildAccesToken = (auth) => {
return buildToken(TOKEN_BASIC, `${new Buffer(auth).toString('base64')}`);
return buildToken(TOKEN_BASIC, `${Buffer.from(auth).toString('base64')}`);
};
/**

@ -2,23 +2,23 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-unused-vars */
import Config from '../../../../packages/config/src/config';
import { generatePackageTemplate } from '@verdaccio/store';
import { IBasicAuth } from '@verdaccio/auth';
import { readFile } from '../../../functional/lib/test.utils';
import { Package } from '@verdaccio/types';
const readMetadata = (fileName: string): Package =>
JSON.parse(readFile(`../../unit/partials/${fileName}`).toString()) as Package;
import {
Config as AppConfig,
IPluginMiddleware,
IStorageManager,
RemoteUser,
IBasicAuth,
} from '@verdaccio/types';
import { IUploadTarball, IReadTarball } from '@verdaccio/streams';
import { generateVersion } from '../../../unit/__helper/utils';
// FIXME: add package here
import Config from '../../../../packages/config/src/config';
const readMetadata = (fileName: string): Package =>
JSON.parse(readFile(`../../unit/partials/${fileName}`).toString()) as Package;
export default class ExampleMiddlewarePlugin implements IPluginMiddleware<{}> {
register_middlewares(app: any, auth: IBasicAuth<{}>, storage: IStorageManager<{}>): void {

@ -13,6 +13,12 @@
{
"path": "../utils"
},
{
"path": "../auth"
},
{
"path": "../store"
},
{
"path": "../mocks"
},

14
pnpm-lock.yaml generated

@ -51,6 +51,7 @@ importers:
babel-plugin-dynamic-import-node: 2.3.3
babel-plugin-emotion: 10.0.33
codecov: 3.6.1
core-js: 3.6.5
cross-env: 7.0.2
detect-secrets: 1.0.6
eslint: 7.5.0
@ -137,6 +138,7 @@ importers:
babel-plugin-dynamic-import-node: 2.3.3
babel-plugin-emotion: 10.0.33
codecov: 3.6.1
core-js: ^3.6.5
cross-env: 7.0.2
detect-secrets: 1.0.6
eslint: 7.5.0
@ -218,6 +220,7 @@ importers:
dependencies:
'@verdaccio/auth': 'link:'
'@verdaccio/commons-api': 'link:../core/commons-api'
'@verdaccio/config': 'link:../config'
'@verdaccio/dev-commons': 'link:../commons'
'@verdaccio/loaders': 'link:../loaders'
'@verdaccio/logger': 'link:../logger'
@ -227,7 +230,6 @@ importers:
jsonwebtoken: 8.5.1
lodash: 4.17.15
devDependencies:
'@verdaccio/config': 'link:../config'
'@verdaccio/mock': 'link:../mock'
'@verdaccio/types': 'link:../core/types'
specifiers:
@ -275,12 +277,14 @@ importers:
'@verdaccio/dev-commons': 'link:../commons'
'@verdaccio/logger': 'link:../logger'
'@verdaccio/utils': 'link:../utils'
debug: 4.2.0
lodash: 4.17.20
mkdirp: 0.5.5
specifiers:
'@verdaccio/dev-commons': 'workspace:5.0.0-alpha.0'
'@verdaccio/logger': 'workspace:5.0.0-alpha.0'
'@verdaccio/utils': 'workspace:5.0.0-alpha.0'
debug: ^4.2.0
lodash: ^4.17.20
mkdirp: 0.5.5
packages/core/commons-api:
@ -475,6 +479,7 @@ importers:
dependencies:
'@verdaccio/dev-commons': 'link:../commons'
'@verdaccio/utils': 'link:../utils'
debug: 4.2.0
fs-extra: 8.1.0
lodash: 4.17.20
request: 2.87.0
@ -486,6 +491,7 @@ importers:
'@verdaccio/dev-commons': 'workspace:5.0.0-alpha.0'
'@verdaccio/types': 'workspace:*'
'@verdaccio/utils': 'workspace:5.0.0-alpha.0'
debug: ^4.2.0
fs-extra: ^8.1.0
lodash: ^4.17.20
request: 2.87.0
@ -497,6 +503,7 @@ importers:
'@verdaccio/logger': 'link:../logger'
'@verdaccio/server': 'link:../server'
'@verdaccio/utils': 'link:../utils'
core-js: 3.6.5
lodash: 4.17.20
selfsigned: 1.10.7
devDependencies:
@ -509,6 +516,7 @@ importers:
'@verdaccio/server': 'workspace:5.0.0-alpha.0'
'@verdaccio/types': 'workspace:*'
'@verdaccio/utils': 'workspace:5.0.0-alpha.0'
core-js: ^3.6.5
lodash: ^4.17.20
selfsigned: 1.10.7
packages/proxy:
@ -651,14 +659,18 @@ importers:
'@verdaccio/utils': 'link:../utils'
verdaccio-htpasswd: 9.7.2
devDependencies:
'@verdaccio/auth': 'link:../auth'
'@verdaccio/dev-commons': 'link:../commons'
'@verdaccio/store': 'link:../store'
specifiers:
'@verdaccio/auth': 'workspace:5.0.0-alpha.0'
'@verdaccio/cli': 'workspace:5.0.0-alpha.0'
'@verdaccio/dev-commons': 'workspace:*'
'@verdaccio/hooks': 'workspace:5.0.0-alpha.0'
'@verdaccio/logger': 'workspace:5.0.0-alpha.0'
'@verdaccio/mock': 'workspace:5.0.0-alpha.0'
'@verdaccio/node-api': 'workspace:5.0.0-alpha.0'
'@verdaccio/store': 'workspace:5.0.0-alpha.0'
'@verdaccio/ui-theme': ^1.12.1
'@verdaccio/utils': 'workspace:5.0.0-alpha.0'
verdaccio-htpasswd: 9.7.2