0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

Merge branch '4.x' of github.com:verdaccio/verdaccio into 4.x

This commit is contained in:
Juan Picado @jotadeveloper 2018-08-21 08:08:48 +02:00
commit 68cb10f44d
No known key found for this signature in database
GPG key ID: 18AC54485952D158
40 changed files with 1084 additions and 285 deletions

5
.gitignore vendored
View file

@ -3,6 +3,10 @@ verdaccio-*.tgz
.DS_Store
build/
### Test
test/unit/partials/store/test-jwt-storage/*
###
!bin/verdaccio
test-storage*
@ -10,7 +14,6 @@ access-storage*
.verdaccio_test_env
node_modules
package-lock.json
build/
npm_test-fails-add-tarball*
yarn-error.log

View file

@ -23,6 +23,16 @@ auth:
# You can set this to -1 to disable registration.
#max_users: 1000
security:
api:
jwt:
sign:
expiresIn: 60d
notBefore: 1
web:
sign:
expiresIn: 7d
# a list of other known repositories we can talk to
uplinks:
npmjs:

View file

@ -27,6 +27,16 @@ auth:
# You can set this to -1 to disable registration.
#max_users: 1000
security:
api:
jwt:
sign:
expiresIn: 60d
notBefore: 1
web:
sign:
expiresIn: 7d
# a list of other known repositories we can talk to
uplinks:
npmjs:

View file

@ -61,6 +61,31 @@ auth:
max_users: 1000
```
### Security
<small>Since: `verdaccio@4.0.0` due [#168](https://github.com/verdaccio/verdaccio/pull/168)</small>
The security block allows you to customise the token signature. To enable [JWT (json web token)](https://jwt.io/) new signture you need to add the block `jwt` to `api` section, `web` uses by default `jwt`.
The configuration is separated in two sections, `api` and `web`. To use JWT on `api`, it has to be defined, otherwise will use the legacy token signature (`aes192`). For JWT you might customize the [signature](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) and the token [verification](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) with your own properties.
```
security:
api:
legacy: true
jwt:
sign:
expiresIn: 29d
verify:
someProp: [value]
web:
sign:
expiresIn: 7d # 7 days by default
verify:
someProp: [value]
```
> We highly recommend move to JWT since legacy signature (`aes192`) is deprecated and will disappear in future versions.
### Web UI
This properties allow you to modify the look and feel of the web UI. For more information about this section read the [web ui page](web.md).

View file

@ -79,7 +79,6 @@ uplinks:
### You Must know
* Verdaccio does not use Basic Authentication since version `v2.3.0`. All tokens generated by verdaccio are based on JWT ([JSON Web Token](https://jwt.io/))
* Uplinks must be registries compatible with the `npm` endpoints. Eg: *verdaccio*, `sinopia@1.4.0`, *npmjs registry*, *yarn registry*, *JFrog*, *Nexus* and more.
* Setting `cache` to false will help to save space in your hard drive. This will avoid store `tarballs` but [it will keep metadata in folders](https://github.com/verdaccio/verdaccio/issues/391).
* Exceed with multiple uplinks might slow down the lookup of your packages due for each request a npm client does, verdaccio does 1 call for each uplink.

View file

@ -32,7 +32,7 @@
"express": "4.16.3",
"global": "4.3.2",
"handlebars": "4.0.11",
"http-errors": "1.6.3",
"http-errors": "1.7.0",
"js-base64": "2.4.8",
"js-string-escape": "1.0.1",
"js-yaml": "3.12.0",
@ -54,7 +54,7 @@
"devDependencies": {
"@commitlint/cli": "7.0.0",
"@commitlint/config-conventional": "7.0.1",
"@verdaccio/types": "3.4.2",
"@verdaccio/types": "3.7.1",
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-eslint": "8.2.6",
@ -195,7 +195,8 @@
},
"lint-staged": {
"*.yaml": [
"prettier --parser yaml --no-config --single-quote --write"
"prettier --parser yaml --no-config --single-quote --write",
"git add"
],
"*.js": [
"eslint .",

View file

@ -1,34 +1,40 @@
// @flow
import type {$Response, Router} from 'express';
import type {$RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth} from '../../../../types';
import {ErrorCode} from '../../../lib/utils';
import _ from 'lodash';
import Cookies from 'cookies';
export default function(route: Router, auth: IAuth) {
import {ErrorCode} from '../../../lib/utils';
import {API_MESSAGE, HTTP_STATUS} from '../../../lib/constants';
import {createSessionToken, getApiToken, getAuthenticatedMessage} from '../../../lib/auth-utils';
import type {Config} from '@verdaccio/types';
import type {$Response, Router} from 'express';
import type {$RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth} from '../../../../types';
export default function(route: Router, auth: IAuth, config: Config) {
route.get('/-/user/:org_couchdb_user', function(req: $RequestExtend, res: $Response, next: $NextFunctionVer) {
res.status(200);
res.status(HTTP_STATUS.OK);
next({
ok: 'you are authenticated as "' + req.remote_user.name + '"',
ok: getAuthenticatedMessage(req.remote_user.name),
});
});
route.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function(req: $RequestExtend, res: $Response, next: $NextFunctionVer) {
let token = (req.body.name && req.body.password)
? auth.aesEncrypt(new Buffer(req.body.name + ':' + req.body.password)).toString('base64')
: undefined;
route.put('/-/user/:org_couchdb_user/:_rev?/:revision?', async function(req: $RequestExtend, res: $Response, next: $NextFunctionVer) {
const {name, password} = req.body;
if (_.isNil(req.remote_user.name) === false) {
res.status(201);
const token = (name && password) ? await getApiToken(auth, config, req.remote_user, password) : undefined;
res.status(HTTP_STATUS.CREATED);
return next({
ok: 'you are authenticated as \'' + req.remote_user.name + '\'',
ok: getAuthenticatedMessage(req.remote_user.name),
token,
});
} else {
auth.add_user(req.body.name, req.body.password, function(err, user) {
auth.add_user(name, password, async function(err, user) {
if (err) {
if (err.status >= 400 && err.status < 500) {
if (err.status >= HTTP_STATUS.BAD_REQUEST && err.status < HTTP_STATUS.INTERNAL_ERROR) {
// With npm registering is the same as logging in,
// and npm accepts only an 409 error.
// So, changing status code here.
@ -37,20 +43,22 @@ export default function(route: Router, auth: IAuth) {
return next(err);
}
const token = (name && password) ? await getApiToken(auth, config, user, password) : undefined;
req.remote_user = user;
res.status(201);
res.status(HTTP_STATUS.CREATED);
return next({
ok: 'user \'' + req.body.name + '\' created',
token: token,
ok: `user '${req.body.name }' created`,
token,
});
});
}
});
route.delete('/-/user/token/*', function(req: $RequestExtend, res: $Response, next: $NextFunctionVer) {
res.status(200);
res.status(HTTP_STATUS.OK);
next({
ok: 'Logged out',
ok: API_MESSAGE.LOGGED_OUT,
});
});
@ -58,10 +66,8 @@ export default function(route: Router, auth: IAuth) {
// placeholder 'cause npm require to be authenticated to publish
// we do not do any real authentication yet
route.post('/_session', Cookies.express(), function(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
res.cookies.set('AuthSession', String(Math.random()), {
// npmjs.org sets 10h expire
expires: new Date(Date.now() + 10 * 60 * 60 * 1000),
});
res.cookies.set('AuthSession', String(Math.random()), createSessionToken());
next({
ok: true,
name: 'somebody',

View file

@ -46,7 +46,7 @@ export default function(config: Config, auth: IAuth, storage: IStorageHandler) {
whoami(app);
pkg(app, auth, storage, config);
search(app, auth, storage);
user(app, auth);
user(app, auth, config);
distTags(app, auth, storage);
publish(app, auth, storage, config);
ping(app);

View file

@ -12,6 +12,8 @@ import apiEndpoint from './endpoint';
import {ErrorCode} from '../lib/utils';
import {API_ERROR, HTTP_STATUS} from '../lib/constants';
import AppConfig from '../lib/config';
import webAPI from './web/api';
import web from './web';
import type {$Application} from 'express';
import type {
@ -74,8 +76,8 @@ const defineAPI = function(config: IConfig, storage: IStorageHandler) {
// For WebUI & WebUI API
if (_.get(config, 'web.enable', true)) {
app.use('/', require('./web')(config, auth, storage));
app.use('/-/verdaccio/', require('./web/api')(config, auth, storage));
app.use('/', web(config, auth, storage));
app.use('/-/verdaccio/', webAPI(config, auth, storage));
} else {
app.get('/', function(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
next(ErrorCode.getNotFound(API_ERROR.WEB_DISABLED));

View file

@ -16,7 +16,7 @@ const route = Router(); /* eslint new-cap: 0 */
/*
This file include all verdaccio only API(Web UI), for npm API please see ../endpoint/
*/
module.exports = function(config: Config, auth: IAuth, storage: IStorageHandler) {
export default function(config: Config, auth: IAuth, storage: IStorageHandler) {
Search.configureStorage(storage);
// validate all of these params as a package name
@ -43,4 +43,4 @@ module.exports = function(config: Config, auth: IAuth, storage: IStorageHandler)
// We will/may replace current token with JWT in next major release, and it will not expire at all(configurable).
return route;
};
}

View file

@ -1,22 +1,29 @@
// @flow
import HTTPError from 'http-errors';
import type {Config} from '@verdaccio/types';
import {HTTP_STATUS} from '../../../lib/constants';
import type {Router} from 'express';
import type {Config, RemoteUser, JWTSignOptions} from '@verdaccio/types';
import type {IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer} from '../../../../types';
import {ErrorCode} from '../../../lib/utils';
import {getSecurity} from '../../../lib/auth-utils';
function addUserAuthApi(route: Router, auth: IAuth, config: Config) {
route.post('/login', function(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
auth.authenticate(req.body.username, req.body.password, (err, user) => {
if (!err) {
const {username, password} = req.body;
auth.authenticate(username, password, async (err, user: RemoteUser) => {
if (err) {
const errorCode = err.message ? HTTP_STATUS.UNAUTHORIZED : HTTP_STATUS.INTERNAL_ERROR;
next(ErrorCode.getCode(errorCode, err.message));
} else {
req.remote_user = user;
const jWTSignOptions: JWTSignOptions = getSecurity(config).web.sign;
next({
token: auth.issueUIjwt(user, '24h'),
token: await auth.jwtEncrypt(user, jWTSignOptions),
username: req.remote_user.name,
});
} else {
next(HTTPError[err.message ? 401 : 500](err.message));
}
});
});

View file

@ -3,11 +3,10 @@ import _ from 'lodash';
import fs from 'fs';
import Search from '../../lib/search';
import * as Utils from '../../lib/utils';
import {WEB_TITLE} from '../../lib/constants';
import {HTTP_STATUS, WEB_TITLE} from '../../lib/constants';
const {securityIframe} = require('../middleware');
/* eslint new-cap:off */
const router = express.Router();
const env = require('../../config/env');
const template = fs.readFileSync(`${env.DIST_PATH}/index.html`).toString();
const spliceURL = require('../../utils/string').spliceURL;
@ -15,6 +14,8 @@ const spliceURL = require('../../utils/string').spliceURL;
module.exports = function(config, auth, storage) {
Search.configureStorage(storage);
const router = express.Router();
router.use(auth.webUIJWTmiddleware());
router.use(securityIframe);
@ -25,7 +26,7 @@ module.exports = function(config, auth, storage) {
if (!err) {
return;
}
if (err.status === 404) {
if (err.status === HTTP_STATUS.NOT_FOUND) {
next();
} else {
next(err);

View file

@ -1,9 +1,60 @@
// @flow
import _ from 'lodash';
import {convertPayloadToBase64, ErrorCode} from './utils';
import {API_ERROR, HTTP_STATUS, ROLES, TIME_EXPIRATION_7D, TOKEN_BASIC, TOKEN_BEARER} from './constants';
import {ErrorCode} from './utils';
import {API_ERROR} from './constants';
import type {
RemoteUser,
Package,
Callback,
Config,
Security,
APITokenOptions,
JWTOptions} from '@verdaccio/types';
import type {
CookieSessionToken, IAuthWebUI, AuthMiddlewarePayload, AuthTokenHeader, BasicPayload,
} from '../../types';
import {aesDecrypt, verifyPayload} from './crypto-utils';
import type {RemoteUser, Package, Callback} from '@verdaccio/types';
/**
* Create a RemoteUser object
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
*/
export function createRemoteUser(name: string, pluginGroups: Array<string>): RemoteUser {
const isGroupValid: boolean = _.isArray(pluginGroups);
const groups = (isGroupValid ? pluginGroups : []).concat([
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL]);
return {
name,
groups,
real_groups: pluginGroups,
};
}
/**
* Builds an anonymous remote user in case none is logged in.
* @return {Object} { name: xx, groups: [], real_groups: [] }
*/
export function createAnonymousRemoteUser(): RemoteUser {
return {
name: undefined,
// groups without '$' are going to be deprecated eventually
groups: [
ROLES.$ALL,
ROLES.$ANONYMOUS,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_ANONUMOUS,
],
real_groups: [],
};
}
export function allow_action(action: string) {
return function(user: RemoteUser, pkg: Package, callback: Callback) {
@ -36,3 +87,162 @@ export function getDefaultPlugins() {
allow_publish: allow_action('publish'),
};
}
export function createSessionToken(): CookieSessionToken {
const tenHoursTime = 10 * 60 * 60 * 1000;
return {
// npmjs.org sets 10h expire
expires: new Date(Date.now() + tenHoursTime),
};
}
const defaultWebTokenOptions: JWTOptions = {
sign: {
expiresIn: TIME_EXPIRATION_7D,
},
verify: {},
};
const defaultApiTokenConf: APITokenOptions = {
legacy: true,
sign: {},
};
export function getSecurity(config: Config): Security {
const defaultSecurity: Security = {
web: defaultWebTokenOptions,
api: defaultApiTokenConf,
};
if (_.isNil(config.security) === false) {
return _.merge(defaultSecurity, config.security);
}
return defaultSecurity;
}
export function getAuthenticatedMessage(user: string): string {
return `you are authenticated as '${user}'`;
}
export function buildUserBuffer(name: string, password: string) {
return Buffer.from(`${name}:${password}`, 'utf8');
}
export function isAESLegacy(security: Security): boolean {
const {legacy, jwt} = security.api;
return _.isNil(legacy) === false &&_.isNil(jwt) && legacy === true;
}
export async function getApiToken(
auth: IAuthWebUI,
config: Config,
remoteUser: RemoteUser,
aesPassword: string): Promise<string> {
const security: Security = getSecurity(config);
if (isAESLegacy(security)) {
// fallback all goes to AES encryption
return await new Promise((resolve) => {
resolve(auth.aesEncrypt(buildUserBuffer((remoteUser: any).name, aesPassword)).toString('base64'));
});
} else {
// i am wiling to use here _.isNil but flow does not like it yet.
const {jwt} = security.api;
if (typeof jwt !== 'undefined' &&
typeof jwt.sign !== 'undefined') {
return await auth.jwtEncrypt(remoteUser, jwt.sign);
} else {
return await new Promise((resolve) => {
resolve(auth.aesEncrypt(buildUserBuffer((remoteUser: any).name, aesPassword)).toString('base64'));
});
}
}
}
export function parseAuthTokenHeader(authorizationHeader: string): AuthTokenHeader {
const parts = authorizationHeader.split(' ');
const [scheme, token] = parts;
return {scheme, token};
}
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 parseAESCredentials(
authorizationHeader: string, secret: string) {
const {scheme, token} = parseAuthTokenHeader(authorizationHeader);
// basic is deprecated and should not be enforced
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
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');
return credentials;
}
}
export function verifyJWTPayload(token: string, secret: string): RemoteUser {
try {
const payload: RemoteUser = (verifyPayload(token, secret): RemoteUser);
return payload;
} catch (err) {
// #168 this check should be removed as soon AES encrypt is removed.
if (err.name === 'JsonWebTokenError') {
// it might be possible the jwt configuration is enabled and
// old tokens fails still remains in usage, thus
// we return an anonymous user to force log in.
return createAnonymousRemoteUser();
} else {
throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, err.message);
}
}
}
export function isAuthHeaderValid(authorization: string): boolean {
return authorization.split(' ').length === 2;
}
export function getMiddlewareCredentials(
security: Security,
secret: string,
authorizationHeader: string
): AuthMiddlewarePayload {
if (isAESLegacy(security)) {
const credentials = parseAESCredentials(authorizationHeader, secret);
if (!credentials) {
return;
}
const parsedCredentials = parseBasicPayload(credentials);
if (!parsedCredentials) {
return;
}
return parsedCredentials;
} else {
const {scheme, token} = parseAuthTokenHeader(authorizationHeader);
if (_.isString(token) && scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
return verifyJWTPayload(token, secret);
}
}
}

View file

@ -2,19 +2,26 @@
import _ from 'lodash';
import {API_ERROR, HTTP_STATUS, ROLES, TOKEN_BASIC, TOKEN_BEARER} from './constants';
import {API_ERROR, TOKEN_BASIC, TOKEN_BEARER} from './constants';
import loadPlugin from '../lib/plugin-loader';
import {buildBase64Buffer, ErrorCode} from './utils';
import {aesDecrypt, aesEncrypt, signPayload, verifyPayload} from './crypto-utils';
import {getDefaultPlugins} from './auth-utils';
import {aesEncrypt, signPayload} from './crypto-utils';
import {
getDefaultPlugins,
getMiddlewareCredentials,
verifyJWTPayload,
createAnonymousRemoteUser,
isAuthHeaderValid,
getSecurity,
isAESLegacy, parseAuthTokenHeader, parseBasicPayload, createRemoteUser,
} from './auth-utils';
import {convertPayloadToBase64, ErrorCode} from './utils';
import {getMatchedPackagesSpec} from './config-utils';
import type {Config, Logger, Callback, IPluginAuth, RemoteUser} from '@verdaccio/types';
import type {
Config, Logger, Callback, IPluginAuth, RemoteUser, JWTSignOptions, Security,
} from '@verdaccio/types';
import type {$Response, NextFunction} from 'express';
import type {$RequestExtend, JWTPayload} from '../../types';
import type {IAuth} from '../../types';
import type {$RequestExtend, IAuth} from '../../types';
const LoggerApi = require('./logger');
@ -23,7 +30,6 @@ class Auth implements IAuth {
logger: Logger;
secret: string;
plugins: Array<any>;
static DEFAULT_EXPIRE_WEB_TOKEN: string = '7d';
constructor(config: Config) {
this.config = config;
@ -50,8 +56,9 @@ class Auth implements IAuth {
this.plugins.push(getDefaultPlugins());
}
authenticate(user: string, password: string, cb: Callback) {
authenticate(username: string, password: string, cb: Callback) {
const plugins = this.plugins.slice(0);
const self = this;
(function next() {
const plugin = plugins.shift();
@ -59,7 +66,8 @@ class Auth implements IAuth {
return next();
}
plugin.authenticate(user, password, function(err, groups) {
self.logger.trace( {username}, 'authenticating @{username}');
plugin.authenticate(username, password, function(err, groups) {
if (err) {
return cb(err);
}
@ -74,14 +82,14 @@ class Auth implements IAuth {
if (!!groups && groups.length !== 0) {
// TODO: create a better understanding of expectations
if (_.isString(groups)) {
throw new TypeError('invalid type for function');
throw new TypeError('plugin group error: invalid type for function');
}
const isGroupValid: boolean = _.isArray(groups);
if (!isGroupValid) {
throw new TypeError(API_ERROR.BAD_FORMAT_USER_GROUP);
}
return cb(err, authenticatedUser(user, groups));
return cb(err, createRemoteUser(username, groups));
}
next();
});
@ -91,6 +99,7 @@ class Auth implements IAuth {
add_user(user: string, password: string, cb: Callback) {
let self = this;
let plugins = this.plugins.slice(0);
this.logger.trace( {user}, 'add user @{user}');
(function next() {
let plugin = plugins.shift();
@ -122,6 +131,7 @@ class Auth implements IAuth {
let plugins = this.plugins.slice(0);
// $FlowFixMe
let pkg = Object.assign({name: packageName}, getMatchedPackagesSpec(packageName, this.config.packages));
this.logger.trace( {packageName}, 'allow access for @{packageName}');
(function next() {
const plugin = plugins.shift();
@ -151,6 +161,7 @@ class Auth implements IAuth {
let plugins = this.plugins.slice(0);
// $FlowFixMe
let pkg = Object.assign({name: packageName}, getMatchedPackagesSpec(packageName, this.config.packages));
this.logger.trace( {packageName}, 'allow publish for @{packageName}');
(function next() {
const plugin = plugins.shift();
@ -188,62 +199,96 @@ class Auth implements IAuth {
return _next();
};
if (_.isUndefined(req.remote_user) === false
&& _.isUndefined(req.remote_user.name) === false) {
if (this._isRemoteUserMissing(req.remote_user)) {
return next();
}
req.remote_user = buildAnonymousUser();
const authorization = req.headers.authorization;
// in case auth header does not exist we return anonymous function
req.remote_user = createAnonymousRemoteUser();
const {authorization} = req.headers;
if (_.isNil(authorization)) {
return next();
}
const parts = authorization.split(' ');
if (parts.length !== 2) {
if (!isAuthHeaderValid(authorization)) {
this.logger.trace('api middleware auth heather is not valid');
return next( ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER) );
}
const credentials = this._parseCredentials(parts);
if (!credentials) {
return next();
const security: Security = getSecurity(this.config);
const {secret} = this.config;
if (isAESLegacy(security)) {
this.logger.trace('api middleware using legacy auth token');
this._handleAESMiddleware(req, security, secret, authorization, next);
} else {
this.logger.trace('api middleware using JWT auth token');
this._handleJWTAPIMiddleware(req, security, secret, authorization, next);
}
};
}
const index = credentials.indexOf(':');
if (index < 0) {
return next();
}
const user: string = credentials.slice(0, index);
const pass: string = credentials.slice(index + 1);
this.authenticate(user, pass, function(err, user) {
_handleJWTAPIMiddleware(
req: $RequestExtend,
security: Security,
secret: string,
authorization: string,
next: Function) {
const {scheme, token} = parseAuthTokenHeader(authorization);
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
// this should happen when client tries to login with an existing user
const credentials = convertPayloadToBase64(token).toString();
const {user, password} = (parseBasicPayload(credentials): any);
this.authenticate(user, password, (err, user) => {
if (!err) {
req.remote_user = user;
next();
} else {
req.remote_user = buildAnonymousUser();
req.remote_user = createAnonymousRemoteUser();
next(err);
}
});
};
} else {
// jwt handler
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
if (credentials) {
// if the signature is valid we rely on it
req.remote_user = credentials;
next();
} else {
// with JWT throw 401
next(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
}
}
}
_parseCredentials(parts: Array<string>) {
let credentials;
const scheme = parts[0];
if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
credentials = buildBase64Buffer(parts[1]).toString();
this.logger.info(API_ERROR.DEPRECATED_BASIC_HEADER);
return credentials;
} else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
const token = buildBase64Buffer(parts[1]);
_handleAESMiddleware(req: $RequestExtend,
security: Security,
secret: string,
authorization: string,
next: Function) {
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
if (credentials) {
const {user, password} = credentials;
this.authenticate(user, password, (err, user) => {
if (!err) {
req.remote_user = user;
next();
} else {
req.remote_user = createAnonymousRemoteUser();
next(err);
}
});
} else {
// we force npm client to ask again with basic authentication
return next(ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER));
}
}
credentials = aesDecrypt(token, this.secret).toString('utf8');
return credentials;
} else {
return;
}
_isRemoteUserMissing(remote_user: RemoteUser): boolean {
return _.isUndefined(remote_user) === false &&
(_.isUndefined(remote_user.name) === false);
}
/**
@ -251,62 +296,65 @@ class Auth implements IAuth {
*/
webUIJWTmiddleware() {
return (req: $RequestExtend, res: $Response, _next: NextFunction) => {
if (_.isNull(req.remote_user) === false && _.isNil(req.remote_user.name) === false) {
if (this._isRemoteUserMissing(req.remote_user)) {
return _next();
}
req.pause();
const next = () => {
const next = (err) => {
req.resume();
if (err) {
// req.remote_user.error = err.message;
res.status(err.statusCode).send(err.message);
}
return _next();
};
const token = (req.headers.authorization || '').replace(`${TOKEN_BEARER} `, '');
const {authorization} = req.headers;
if (_.isNil(authorization)) {
return next();
}
if (!isAuthHeaderValid(authorization)) {
return next( ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER) );
}
const token = (authorization || '').replace(`${TOKEN_BEARER} `, '');
if (!token) {
return next();
}
let decoded;
let credentials;
try {
decoded = this.decode_token(token);
credentials = verifyJWTPayload(token, this.config.secret);
} catch (err) {
// FIXME: intended behaviour, do we want it?
}
if (decoded) {
req.remote_user = authenticatedUser(decoded.user, decoded.group);
if (credentials) {
const {name, groups} = credentials;
// $FlowFixMe
req.remote_user = createRemoteUser(name, groups);
} else {
req.remote_user = buildAnonymousUser();
req.remote_user = createAnonymousRemoteUser();
}
next();
};
}
issueUIjwt(user: any, expiresIn: string) {
const {name, real_groups} = user;
const payload: JWTPayload = {
user: name,
async jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): string {
const {real_groups} = user;
const payload: RemoteUser = {
...user,
group: real_groups && real_groups.length ? real_groups : undefined,
};
return signPayload(payload, this.secret, {expiresIn: expiresIn || Auth.DEFAULT_EXPIRE_WEB_TOKEN});
}
const token: string = await signPayload(payload, this.secret, signOptions);
/**
* Decodes the token.
* @param {*} token
* @return {Object}
*/
decode_token(token: string) {
let decoded;
try {
decoded = verifyPayload(token, this.secret);
} catch (err) {
throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, err.message);
}
return decoded;
// $FlowFixMe
return token;
}
/**
@ -317,37 +365,4 @@ class Auth implements IAuth {
}
}
/**
* Builds an anonymous user in case none is logged in.
* @return {Object} { name: xx, groups: [], real_groups: [] }
*/
function buildAnonymousUser() {
return {
name: undefined,
// groups without '$' are going to be deprecated eventually
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONUMOUS],
real_groups: [],
};
}
/**
* Authenticate an user.
* @return {Object} { name: xx, pluginGroups: [], real_groups: [] }
*/
function authenticatedUser(name: string, pluginGroups: Array<any>) {
const isGroupValid: boolean = _.isArray(pluginGroups);
const groups = (isGroupValid ? pluginGroups : []).concat([
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL]);
return {
name,
groups,
real_groups: pluginGroups,
};
}
export default Auth;

View file

@ -15,6 +15,7 @@ import {APP_ERROR} from './constants';
import type {
PackageList,
Config as AppConfig,
Security,
Logger,
} from '@verdaccio/types';
@ -38,6 +39,7 @@ class Config implements AppConfig {
self_path: string;
storage: string | void;
plugins: string | void;
security: Security;
$key: any;
$value: any;

View file

@ -1,7 +1,9 @@
// @flow
export const DEFAULT_PORT = '4873';
export const DEFAULT_DOMAIN = 'localhost';
export const DEFAULT_PORT: string = '4873';
export const DEFAULT_DOMAIN: string = 'localhost';
export const TIME_EXPIRATION_24H: string ='24h';
export const TIME_EXPIRATION_7D: string = '7d';
export const HEADERS = {
JSON: 'application/json',
@ -63,6 +65,7 @@ export const API_MESSAGE = {
TAG_UPDATED: 'tags updated',
TAG_REMOVED: 'tag removed',
TAG_ADDED: 'package tagged',
LOGGED_OUT: 'Logged out',
};
export const API_ERROR = {

View file

@ -1,12 +1,21 @@
// @flow
import {createDecipher, createCipher, createHash, pseudoRandomBytes} from 'crypto';
import {
createDecipher,
createCipher,
createHash,
pseudoRandomBytes,
} from 'crypto';
import jwt from 'jsonwebtoken';
import type {JWTPayload, JWTSignOptions} from '../../types';
import type {JWTSignOptions, RemoteUser} from '@verdaccio/types';
export const defaultAlgorithm = 'aes192';
export const defaultTarballHashAlgorithm = 'sha1';
export function aesEncrypt(buf: Buffer, secret: string): Buffer {
// deprecated
// https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options
const c = createCipher(defaultAlgorithm, secret);
const b1 = c.update(buf);
const b2 = c.final();
@ -16,6 +25,8 @@ export function aesEncrypt(buf: Buffer, secret: string): Buffer {
export function aesDecrypt(buf: Buffer, secret: string) {
try {
// deprecated
// https://nodejs.org/api/crypto.html#crypto_crypto_createdecipher_algorithm_password_options
const c = createDecipher(defaultAlgorithm, secret);
const b1 = c.update(buf);
const b2 = c.final();
@ -26,7 +37,7 @@ export function aesDecrypt(buf: Buffer, secret: string) {
}
export function createTarballHash() {
return createHash('sha1');
return createHash(defaultTarballHashAlgorithm);
}
/**
@ -44,13 +55,18 @@ export function generateRandomHexString(length: number = 8) {
return pseudoRandomBytes(length).toString('hex');
}
export function signPayload(payload: JWTPayload, secret: string, options: JWTSignOptions) {
return jwt.sign(payload, secret, {
notBefore: '1', // Make sure the time will not rollback :)
...options,
export async function signPayload(
payload: RemoteUser,
secretOrPrivateKey: string,
options: JWTSignOptions): Promise<string> {
return new Promise(function(resolve, reject) {
return jwt.sign(payload, secretOrPrivateKey, {
notBefore: '1000', // Make sure the time will not rollback :)
...options,
}, (error, token) => error ? reject(error) : resolve(token));
});
}
export function verifyPayload(token: string, secret: string) {
return jwt.verify(token, secret);
export function verifyPayload(token: string, secretOrPrivateKey: string) {
return jwt.verify(token, secretOrPrivateKey);
}

View file

@ -34,7 +34,7 @@ export function getUserAgent(): string {
return `${pkgName}/${pkgVersion}`;
}
export function buildBase64Buffer(payload: string): Buffer {
export function convertPayloadToBase64(payload: string): Buffer {
return new Buffer(payload, 'base64');
}

View file

@ -10,7 +10,7 @@ class API {
if (token) {
if (!options.headers) options.headers = {};
options.headers.authorization = token;
options.headers.authorization = `Bearer ${token}`;
}
if (!['http://', 'https://', '//'].some((prefix) => url.startsWith(prefix))) {

View file

@ -1,10 +1,10 @@
import fs from 'fs';
import path from 'path';
import assert from 'assert';
import crypto from 'crypto';
import {readFile} from '../lib/test.utils';
import {HTTP_STATUS} from "../../../src/lib/constants";
import {TARBALL} from '../config.functional';
import {createTarballHash} from '../../../src/lib/crypto-utils';
function getBinary() {
return readFile('../fixtures/binary');
@ -35,7 +35,7 @@ export default function (server, server2, server3) {
beforeAll(function () {
const pkg = require('../fixtures/package')(PKG_GH131);
pkg.dist.shasum = crypto.createHash('sha1').update(getBinary()).digest('hex');
pkg.dist.shasum = createTarballHash().update(getBinary()).digest('hex');
return server.putVersion(PKG_GH131, '0.0.1', pkg)
.status(HTTP_STATUS.CREATED)
@ -67,7 +67,7 @@ export default function (server, server2, server3) {
beforeAll(function () {
const pkg = require('../fixtures/package')(PKG_GH1312);
pkg.dist.shasum = crypto.createHash('sha1').update(getBinary()).digest('hex');
pkg.dist.shasum = createTarballHash().update(getBinary()).digest('hex');
return server2.putVersion(PKG_GH1312, '0.0.1', pkg)
.status(HTTP_STATUS.CREATED)

View file

@ -116,7 +116,6 @@ function smartRequest(options: any): Promise<any> {
// store the response on symbol
smartObject[requestData].response = res;
// console.log("======>smartRequest RESPONSE: ", body);
resolve(body);
});
});

View file

@ -68,25 +68,14 @@ export default class VerdaccioProcess implements IServerProcess {
this.bridge.auth(CREDENTIALS.user, CREDENTIALS.password)
.status(HTTP_STATUS.CREATED)
.body_ok(new RegExp(CREDENTIALS.user))
.then(() => {
resolve([this, body.pid]);
}, reject)
.then(() => resolve([this, body.pid]), reject)
}, reject);
}
});
this.childFork.on('error', (err) => {
console.log('error process', err);
reject([err, this]);
});
this.childFork.on('disconnect', (err) => {
reject([err, this]);
});
this.childFork.on('exit', (err) => {
reject([err, this]);
});
this.childFork.on('error', (err) => reject([err, this]));
this.childFork.on('disconnect', (err) => reject([err, this]));
this.childFork.on('exit', (err) => reject([err, this]));
}
stop(): void {

5
test/unit/__helper.js Normal file
View file

@ -0,0 +1,5 @@
import path from 'path';
export const parseConfigurationFile = (name) => {
return path.join(__dirname, `./partials/config/yaml/${name}.yaml`);
};

View file

@ -0,0 +1,32 @@
// @flow
import {HEADER_TYPE, HEADERS, HTTP_STATUS} from '../../../src/lib/constants';
export function getPackage(
request: any,
header: string,
pkg: string,
statusCode: number = HTTP_STATUS.OK) {
return new Promise((resolve) => {
request.get(`/${pkg}`)
.set('authorization', header)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(statusCode)
.end(function(err, res) {
resolve([err, res]);
});
});
}
export function addUser(request: any, user: string, credentials: any,
statusCode: number = HTTP_STATUS.CREATED) {
return new Promise((resolve, reject) => {
request.put(`/-/user/org.couchdb.user:${user}`)
.send(credentials)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(statusCode)
.end(function(err, res) {
return resolve([err, res]);
});
});
}

View file

@ -0,0 +1,115 @@
// @flow
import request from 'supertest';
import _ from 'lodash';
import path from 'path';
import rimraf from 'rimraf';
import Config from '../../../src/lib/config';
import endPointAPI from '../../../src/api/index';
import {HEADERS, HTTP_STATUS, HEADER_TYPE} from '../../../src/lib/constants';
import {mockServer} from './mock';
import {DOMAIN_SERVERS} from '../../functional/config.functional';
import {parseConfigFile} from '../../../src/lib/utils';
import {parseConfigurationFile} from '../__helper';
import {addUser, getPackage} from './__api-helper';
import {setup} from '../../../src/lib/logger';
import {buildUserBuffer} from '../../../src/lib/auth-utils';
setup([]);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const parseConfigurationJWTFile = () => {
return parseConfigurationFile(`api-jwt/jwt`);
};
const FORBIDDEN_VUE: string = 'unregistered users are not allowed to access package vue';
describe('endpoint user auth JWT unit test', () => {
let config;
let app;
let mockRegistry;
beforeAll(function(done) {
const store = path.join(__dirname, '../partials/store/test-jwt-storage');
const mockServerPort = 55546;
rimraf(store, async () => {
const confS = parseConfigFile(parseConfigurationJWTFile());
const configForTest = _.clone(confS);
configForTest.storage = store;
configForTest.auth = {
htpasswd: {
file: './test-jwt-storage/.htpasswd'
}
};
configForTest.uplinks = {
npmjs: {
url: `http://${DOMAIN_SERVERS}:${mockServerPort}`
}
};
configForTest.self_path = store;
config = new Config(configForTest);
app = await endPointAPI(config);
mockRegistry = await mockServer(mockServerPort).init();
done();
});
});
afterAll(function(done) {
mockRegistry[0].stop();
done();
});
test('should test add a new user with JWT enabled', async (done) => {
const [err, res] = await addUser(request(app), credentials.name, credentials);
expect(err).toBeNull();
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 '${credentials.name}' created`);
// testing JWT auth headers with token
// we need it here, because token is required
const [err1, resp1] = await getPackage(request(app), `Bearer ${token}`, 'vue');
expect(err1).toBeNull();
expect(resp1.body).toBeDefined();
expect(resp1.body.name).toMatch('vue');
const [err2, resp2] = await getPackage(request(app), `Bearer fake`, 'vue', HTTP_STATUS.FORBIDDEN);
expect(err2).toBeNull();
expect(resp2.statusCode).toBe(HTTP_STATUS.FORBIDDEN);
expect(resp2.body.error).toMatch(FORBIDDEN_VUE);
done();
});
test('should emulate npm login when user already exist', async (done) => {
const credentials = { name: 'jwtUser2', password: 'secretPass' };
// creates an user
await addUser(request(app), credentials.name, credentials);
// it should fails conflict 409
await addUser(request(app), credentials.name, credentials, HTTP_STATUS.CONFLICT);
// npm will try to sign in sending credentials via basic auth header
const token = buildUserBuffer(credentials.name, credentials.password).toString('base64');
request(app).put(`/-/user/org.couchdb.user:${credentials.name}/-rev/undefined`)
.send(credentials)
.set('authorization', `Basic ${token}`)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED)
.end(function(err, res) {
expect(err).toBeNull();
expect(res.body.ok).toBeDefined();
expect(res.body.token).toBeDefined();
done();
});
});
test('should fails on try to access with corrupted token', async (done) => {
const [err2, resp2] = await getPackage(request(app), `Bearer fake`, 'vue', HTTP_STATUS.FORBIDDEN);
expect(err2).toBeNull();
expect(resp2.statusCode).toBe(HTTP_STATUS.FORBIDDEN);
expect(resp2.body.error).toMatch(FORBIDDEN_VUE);
done();
});
});

View file

@ -0,0 +1,249 @@
// @flow
import _ from 'lodash';
import Auth from '../../../src/lib/auth';
// $FlowFixMe
import configExample from '../partials/config/index';
import AppConfig from '../../../src/lib/config';
import {setup} from '../../../src/lib/logger';
import {convertPayloadToBase64, parseConfigFile} from '../../../src/lib/utils';
import {
buildUserBuffer,
getApiToken,
getAuthenticatedMessage,
getMiddlewareCredentials,
getSecurity
} from '../../../src/lib/auth-utils';
import {aesDecrypt, verifyPayload} from '../../../src/lib/crypto-utils';
import {parseConfigurationFile} from '../__helper';
import type {IAuth, } from '../../../types/index';
import type {Config, Security, RemoteUser} from '@verdaccio/types';
setup(configExample.logs);
describe('Auth utilities', () => {
const parseConfigurationSecurityFile = (name) => {
return parseConfigurationFile(`security/${name}`);
};
function getConfig(configFileName: string, secret: string) {
const conf = parseConfigFile(parseConfigurationSecurityFile(configFileName));
const secConf= _.merge(configExample, conf);
secConf.secret = secret;
const config: Config = new AppConfig(secConf);
return config;
}
async function signCredentials(
configFileName: string,
username: string,
password: string,
secret = '12345',
methodToSpy: string,
methodNotBeenCalled: string): Promise<string> {
const config: Config = getConfig(configFileName, secret);
const auth: IAuth = new Auth(config);
const spy = jest.spyOn(auth, methodToSpy);
const spyNotCalled = jest.spyOn(auth, methodNotBeenCalled);
const user: RemoteUser = {
name: username,
real_groups: [],
groups: []
};
const token = await getApiToken(auth, config, user, password);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
expect(spyNotCalled).not.toHaveBeenCalled();
expect(token).toBeDefined();
return token;
}
const verifyJWT = (token: string, user: string, password: string, secret: string) => {
const payload = verifyPayload(token, secret);
expect(payload.name).toBe(user);
expect(payload.groups).toBeDefined();
expect(payload.real_groups).toBeDefined();
};
const verifyAES = (token: string, user: string, password: string, secret: string) => {
const payload = aesDecrypt(convertPayloadToBase64(token), secret).toString('utf8');
const content = payload.split(':');
expect(content[0]).toBe(user);
expect(content[0]).toBe(password);
};
describe('getApiToken test', () => {
test('should sign token with aes and security missing', async () => {
const token = await signCredentials('security-missing',
'test', 'test', '1234567', 'aesEncrypt', 'jwtEncrypt');
verifyAES(token, 'test', 'test', '1234567');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with aes and security emtpy', async () => {
const token = await signCredentials('security-empty',
'test', 'test', '123456', 'aesEncrypt', 'jwtEncrypt');
verifyAES(token, 'test', 'test', '123456');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with aes', async () => {
const token = await signCredentials('security-basic',
'test', 'test', '123456', 'aesEncrypt', 'jwtEncrypt');
verifyAES(token, 'test', 'test', '123456');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with legacy and jwt disabled', async () => {
const token = await signCredentials('security-no-legacy',
'test', 'test', 'x8T#ZCx=2t', 'aesEncrypt', 'jwtEncrypt');
expect(_.isString(token)).toBeTruthy();
verifyAES(token, 'test', 'test', 'x8T#ZCx=2t');
});
test('should sign token with legacy enabled and jwt enabled', async () => {
const token = await signCredentials('security-jwt-legacy-enabled',
'test', 'test', 'secret', 'jwtEncrypt', 'aesEncrypt');
verifyJWT(token, 'test', 'test', 'secret');
expect(_.isString(token)).toBeTruthy();
});
test('should sign token with jwt enabled', async () => {
const token = await signCredentials('security-jwt',
'test', 'test', 'secret', 'jwtEncrypt', 'aesEncrypt');
expect(_.isString(token)).toBeTruthy();
verifyJWT(token, 'test', 'test', 'secret');
});
test('should sign with jwt whether legacy is disabled', async () => {
const token = await signCredentials('security-legacy-disabled',
'test', 'test', 'secret', 'jwtEncrypt', 'aesEncrypt');
expect(_.isString(token)).toBeTruthy();
verifyJWT(token, 'test', 'test', 'secret');
});
});
describe('getAuthenticatedMessage test', () => {
test('should sign token with jwt enabled', () => {
expect(getAuthenticatedMessage('test')).toBe('you are authenticated as \'test\'');
});
});
describe('getMiddlewareCredentials test', () => {
describe('should get AES credentials', () => {
test.concurrent('should unpack aes token and credentials', async () => {
const secret: string = 'secret';
const user: string = 'test';
const pass: string = 'test';
const token = await signCredentials('security-legacy',
user, pass, secret, 'aesEncrypt', 'jwtEncrypt');
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `Bearer ${token}`);
expect(credentials).toBeDefined();
// $FlowFixMe
expect(credentials.user).toEqual(user);
// $FlowFixMe
expect(credentials.password).toEqual(pass);
});
test.concurrent('should unpack aes token and credentials', async () => {
const secret: string = 'secret';
const user: string = 'test';
const pass: string = 'test';
const token = buildUserBuffer(user, pass).toString('base64');
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `Basic ${token}`);
expect(credentials).toBeDefined();
// $FlowFixMe
expect(credentials.user).toEqual(user);
// $FlowFixMe
expect(credentials.password).toEqual(pass);
});
test.concurrent('should return empty credential wrong secret key', async () => {
const secret: string = 'secret';
const token = await signCredentials('security-legacy',
'test', 'test', secret, 'aesEncrypt', 'jwtEncrypt');
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, 'BAD_SECRET', `Bearer ${token}`);
expect(credentials).not.toBeDefined();
});
test.concurrent('should return empty credential wrong scheme', async () => {
const secret: string = 'secret';
const token = await signCredentials('security-legacy',
'test', 'test', secret, 'aesEncrypt', 'jwtEncrypt');
const config: Config = getConfig('security-legacy', secret);
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `BAD_SCHEME ${token}`);
expect(credentials).not.toBeDefined();
});
test.concurrent('should return empty credential corrupted payload', async () => {
const secret: string = 'secret';
const config: Config = getConfig('security-legacy', secret);
const auth: IAuth = new Auth(config);
const token = auth.aesEncrypt(new Buffer(`corruptedBuffer`)).toString('base64');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `Bearer ${token}`);
expect(credentials).not.toBeDefined();
});
});
describe('should get JWT credentials', () => {
test('should return anonymous whether token is corrupted', () => {
const config: Config = getConfig('security-jwt', '12345');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, '12345', 'Bearer fakeToken');
expect(credentials).toBeDefined();
// $FlowFixMe
expect(credentials.name).not.toBeDefined();
// $FlowFixMe
expect(credentials.real_groups).toBeDefined();
// $FlowFixMe
expect(credentials.real_groups).toEqual([]);
});
test('should return anonymous whether token and scheme are corrupted', () => {
const config: Config = getConfig('security-jwt', '12345');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, '12345', 'FakeScheme fakeToken');
expect(credentials).not.toBeDefined();
});
test('should verify succesfully a JWT token', async () => {
const secret: string = 'secret';
const user: string = 'test';
const config: Config = getConfig('security-jwt', secret);
const token = await signCredentials('security-jwt',
user, 'secretTest', secret, 'jwtEncrypt', 'aesEncrypt');
const security: Security = getSecurity(config);
const credentials = getMiddlewareCredentials(security, secret, `Bearer ${token}`);
expect(credentials).toBeDefined();
// $FlowFixMe
expect(credentials.name).toEqual(user);
// $FlowFixMe
expect(credentials.real_groups).toBeDefined();
// $FlowFixMe
expect(credentials.real_groups).toEqual([]);
});
});
});
});

View file

@ -11,107 +11,111 @@ import {setup} from '../../../src/lib/logger';
import type {IAuth} from '../../../types/index';
import type {Config} from '@verdaccio/types';
const authConfig = Object.assign({}, configExample);
// avoid noisy log output
authConfig.logs = [{type: 'stdout', format: 'pretty', level: 'error'}];
setup(configExample.logs);
describe('AuthTest', () => {
test('should be defined', () => {
const config: Config = new AppConfig(configExample);
const auth: IAuth = new Auth(config);
test('should be defined', () => {
const config: Config = new AppConfig(authConfig);
const auth: IAuth = new Auth(config);
expect(auth).toBeDefined();
});
expect(auth).toBeDefined();
});
describe('authenticate', () => {
test('should utilize plugin', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
describe('test authenticate method', () => {
test('should utilize plugin', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
expect(auth).toBeDefined();
expect(auth).toBeDefined();
const callback = jest.fn();
const result = [ "test" ];
const callback = jest.fn();
const result = [ "test" ];
// $FlowFixMe
auth.authenticate(1, null, callback);
// $FlowFixMe
auth.authenticate(null, result, callback);
// $FlowFixMe
auth.authenticate(1, null, callback);
// $FlowFixMe
auth.authenticate(null, result, callback);
expect(callback.mock.calls).toHaveLength(2);
expect(callback.mock.calls[0][0]).toBe(1);
expect(callback.mock.calls[0][1]).toBeUndefined();
expect(callback.mock.calls[1][0]).toBeNull();
expect(callback.mock.calls[1][1].real_groups).toBe(result);
});
expect(callback.mock.calls).toHaveLength(2);
expect(callback.mock.calls[0][0]).toBe(1);
expect(callback.mock.calls[0][1]).toBeUndefined();
expect(callback.mock.calls[1][0]).toBeNull();
expect(callback.mock.calls[1][1].real_groups).toBe(result);
});
test('should skip falsy values', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
test('should skip falsy values', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
expect(auth).toBeDefined();
expect(auth).toBeDefined();
const callback = jest.fn();
let index = 0;
const callback = jest.fn();
let index = 0;
// as defined by https://developer.mozilla.org/en-US/docs/Glossary/Falsy
for (const value of [ false, 0, "", null, undefined, NaN ]) {
// $FlowFixMe
auth.authenticate(null, value, callback);
const call = callback.mock.calls[index++];
expect(call[0]).toBeDefined();
expect(call[1]).toBeUndefined();
}
});
// as defined by https://developer.mozilla.org/en-US/docs/Glossary/Falsy
for (const value of [ false, 0, "", null, undefined, NaN ]) {
// $FlowFixMe
auth.authenticate(null, value, callback);
const call = callback.mock.calls[index++];
expect(call[0]).toBeDefined();
expect(call[1]).toBeUndefined();
}
});
test('should error truthy non-array', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
test('should error truthy non-array', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
expect(auth).toBeDefined();
expect(auth).toBeDefined();
const callback = jest.fn();
const callback = jest.fn();
for (const value of [ true, 1, "test", { } ]) {
expect(function ( ) {
// $FlowFixMe
auth.authenticate(null, value, callback);
}).toThrow(TypeError);
expect(callback.mock.calls).toHaveLength(0);
}
});
for (const value of [ true, 1, "test", { } ]) {
expect(function ( ) {
// $FlowFixMe
auth.authenticate(null, value, callback);
}).toThrow(TypeError);
expect(callback.mock.calls).toHaveLength(0);
}
});
test('should skip empty array', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
test('should skip empty array', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
expect(auth).toBeDefined();
expect(auth).toBeDefined();
const callback = jest.fn();
const value = [ ];
const callback = jest.fn();
const value = [ ];
// $FlowFixMe
auth.authenticate(null, value, callback);
expect(callback.mock.calls).toHaveLength(1);
expect(callback.mock.calls[0][0]).toBeDefined();
expect(callback.mock.calls[0][1]).toBeUndefined();
});
// $FlowFixMe
auth.authenticate(null, value, callback);
expect(callback.mock.calls).toHaveLength(1);
expect(callback.mock.calls[0][0]).toBeDefined();
expect(callback.mock.calls[0][1]).toBeUndefined();
});
test('should accept valid array', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
test('should accept valid array', () => {
const config: Config = new AppConfig(configPlugins);
const auth: IAuth = new Auth(config);
expect(auth).toBeDefined();
expect(auth).toBeDefined();
const callback = jest.fn();
let index = 0;
const callback = jest.fn();
let index = 0;
for (const value of [ [ "" ], [ "1" ], [ "0" ], ["000"] ]) {
// $FlowFixMe
auth.authenticate(null, value, callback);
const call = callback.mock.calls[index++];
expect(call[0]).toBeNull();
expect(call[1].real_groups).toBe(value);
}
});
})
for (const value of [ [ "" ], [ "1" ], [ "0" ], ["000"] ]) {
// $FlowFixMe
auth.authenticate(null, value, callback);
const call = callback.mock.calls[index++];
expect(call[0]).toBeNull();
expect(call[1].real_groups).toBe(value);
}
});
})
});

View file

@ -13,19 +13,19 @@ import {PACKAGE_ACCESS, ROLES} from '../../../src/lib/constants';
describe('Config Utilities', () => {
const parsePartial = (name) => {
const parseConfigurationFile = (name) => {
return path.join(__dirname, `../partials/config/yaml/${name}.yaml`);
};
describe('uplinkSanityCheck', () => {
test('should test basic conversion', ()=> {
const uplinks = uplinkSanityCheck(parseConfigFile(parsePartial('uplink-basic')).uplinks);
const uplinks = uplinkSanityCheck(parseConfigFile(parseConfigurationFile('uplink-basic')).uplinks);
expect(Object.keys(uplinks)).toContain('server1');
expect(Object.keys(uplinks)).toContain('server2');
});
test('should throw error on blacklisted uplink name', ()=> {
const {uplinks} = parseConfigFile(parsePartial('uplink-wrong'));
const {uplinks} = parseConfigFile(parseConfigurationFile('uplink-wrong'));
expect(() => {
uplinkSanityCheck(uplinks)
@ -35,7 +35,7 @@ describe('Config Utilities', () => {
describe('sanityCheckUplinksProps', () => {
test('should fails if url prop is missing', ()=> {
const {uplinks} = parseConfigFile(parsePartial('uplink-wrong'));
const {uplinks} = parseConfigFile(parseConfigurationFile('uplink-wrong'));
expect(() => {
sanityCheckUplinksProps(uplinks)
}).toThrow('CONFIG: no url for uplink: none-url');
@ -48,7 +48,7 @@ describe('Config Utilities', () => {
describe('normalisePackageAccess', () => {
test('should test basic conversion', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-basic'));
const {packages} = parseConfigFile(parseConfigurationFile('pkgs-basic'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
@ -60,7 +60,7 @@ describe('Config Utilities', () => {
});
test('should test multi group', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-multi-group'));
const {packages} = parseConfigFile(parseConfigurationFile('pkgs-multi-group'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
@ -84,7 +84,7 @@ describe('Config Utilities', () => {
test('should deprecated packages props', ()=> {
const {packages} = parseConfigFile(parsePartial('deprecated-pkgs-basic'));
const {packages} = parseConfigFile(parseConfigurationFile('deprecated-pkgs-basic'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
@ -118,7 +118,7 @@ describe('Config Utilities', () => {
});
test('should check not default packages access', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-empty'));
const {packages} = parseConfigFile(parseConfigurationFile('pkgs-empty'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
@ -137,7 +137,7 @@ describe('Config Utilities', () => {
describe('getMatchedPackagesSpec', () => {
test('should test basic config', () => {
const {packages} = parseConfigFile(parsePartial('pkgs-custom'));
const {packages} = parseConfigFile(parseConfigurationFile('pkgs-custom'));
// $FlowFixMe
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
// $FlowFixMe
@ -149,7 +149,7 @@ describe('Config Utilities', () => {
});
test('should test no ** wildcard on config', () => {
const {packages} = parseConfigFile(parsePartial('pkgs-nosuper-wildcard-custom'));
const {packages} = parseConfigFile(parseConfigurationFile('pkgs-nosuper-wildcard-custom'));
// $FlowFixMe
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
// $FlowFixMe
@ -163,7 +163,7 @@ describe('Config Utilities', () => {
describe('hasProxyTo', () => {
test('should test basic config', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-basic')).packages);
const packages = normalisePackageAccess(parseConfigFile(parseConfigurationFile('pkgs-basic')).packages);
// react
expect(hasProxyTo('react', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('react', 'google', packages)).toBeFalsy();
@ -178,7 +178,7 @@ describe('Config Utilities', () => {
});
test('should test resolve based on custom package access', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-custom')).packages);
const packages = normalisePackageAccess(parseConfigFile(parseConfigurationFile('pkgs-custom')).packages);
// react
expect(hasProxyTo('react', 'facebook', packages)).toBeTruthy();
expect(hasProxyTo('react', 'google', packages)).toBeFalsy();
@ -193,7 +193,7 @@ describe('Config Utilities', () => {
});
test('should not resolve any proxy', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-empty')).packages);
const packages = normalisePackageAccess(parseConfigFile(parseConfigurationFile('pkgs-empty')).packages);
// react
expect(hasProxyTo('react', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('react', 'npmjs', packages)).toBeFalsy();

View file

@ -0,0 +1,36 @@
storage: ./storage
plugins: ./plugins
web:
title: Verdaccio
auth:
htpasswd:
file: ./htpasswd
uplinks:
npmjs:
url: https://registry.npmjs.org/
security:
api:
jwt:
sign:
expiresIn: 10m
notBefore: 0
packages:
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'vue':
access: $authenticated
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs
middlewares:
audit:
enabled: true
logs:
- { type: stdout, format: pretty, level: http }

View file

@ -0,0 +1,12 @@
security:
api:
legacy: true # use AES algorithm
# jwt enables json web token and disable legacy
# jwt: https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
sign:
expiresIn: 7d # 7 days by default
# verify:
web:
sign:
expiresIn: 7d # 7 days by default
# verify: https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback

View file

@ -0,0 +1 @@
? security

View file

@ -0,0 +1,10 @@
security:
api:
legacy: true
jwt:
sign:
expiresIn: 7d
notBefore: 0
web:
sign:
expiresIn: 7d

View file

@ -0,0 +1,6 @@
security:
api:
jwt:
sign:
expiresIn: 7d
notBefore: 0

View file

@ -0,0 +1,3 @@
security:
api:
legacy: false

View file

@ -0,0 +1,3 @@
security:
api:
legacy: true

View file

@ -0,0 +1,9 @@
security:
api:
legacy: false
sign:
expiresIn: 7d
notBefore: 0
web:
sign:
expiresIn: 7d

View file

@ -0,0 +1 @@
{"list":[],"secret":"c884521349204fdb6665e27a20c04dd1fb81863a56fa68cbbab39aebea0dd609"}

View file

@ -8,8 +8,10 @@ import type {
Callback,
Versions,
Version,
RemoteUser,
Config,
Logger,
JWTSignOptions,
PackageAccess,
StringValue as verdaccio$StringValue,
Package} from '@verdaccio/types';
@ -30,19 +32,31 @@ export type StartUpConfig = {
export type MatchedPackage = PackageAccess | void;
export type JWTPayload = {
user: string;
group: string | void;
export type JWTPayload = RemoteUser & {
password?: string;
}
export type JWTSignOptions = {
expiresIn: string;
export type AESPayload = {
user: string;
password: string;
}
export type AuthTokenHeader = {
scheme: string;
token: string;
}
export type BasicPayload = AESPayload | void;
export type AuthMiddlewarePayload = RemoteUser | BasicPayload;
export type ProxyList = {
[key: string]: IProxy;
}
export type CookieSessionToken = {
expires: Date;
}
export type Utils = {
ErrorCode: any;
getLatestVersion: Callback;
@ -59,8 +73,9 @@ export type $NextFunctionVer = NextFunction & mixed;
export type $SidebarPackage = Package & {latest: mixed}
interface IAuthWebUI {
issueUIjwt(user: string, time: string): string;
export interface IAuthWebUI {
jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): string;
aesEncrypt(buf: Buffer): Buffer;
}
interface IAuthMiddleware {

BIN
yarn.lock

Binary file not shown.