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:
commit
68cb10f44d
40 changed files with 1084 additions and 285 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 .",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
233
src/lib/auth.js
233
src/lib/auth.js
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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))) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
5
test/unit/__helper.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import path from 'path';
|
||||
|
||||
export const parseConfigurationFile = (name) => {
|
||||
return path.join(__dirname, `./partials/config/yaml/${name}.yaml`);
|
||||
};
|
32
test/unit/api/__api-helper.js
Normal file
32
test/unit/api/__api-helper.js
Normal 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]);
|
||||
});
|
||||
});
|
||||
}
|
115
test/unit/api/api.jwt.spec.js
Normal file
115
test/unit/api/api.jwt.spec.js
Normal 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();
|
||||
});
|
||||
|
||||
});
|
249
test/unit/api/auth-utils.spec.js
Normal file
249
test/unit/api/auth-utils.spec.js
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
36
test/unit/partials/config/yaml/api-jwt/jwt.yaml
Normal file
36
test/unit/partials/config/yaml/api-jwt/jwt.yaml
Normal 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 }
|
12
test/unit/partials/config/yaml/security/security-basic.yaml
Normal file
12
test/unit/partials/config/yaml/security/security-basic.yaml
Normal 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
|
|
@ -0,0 +1 @@
|
|||
? security
|
|
@ -0,0 +1,10 @@
|
|||
security:
|
||||
api:
|
||||
legacy: true
|
||||
jwt:
|
||||
sign:
|
||||
expiresIn: 7d
|
||||
notBefore: 0
|
||||
web:
|
||||
sign:
|
||||
expiresIn: 7d
|
|
@ -0,0 +1,6 @@
|
|||
security:
|
||||
api:
|
||||
jwt:
|
||||
sign:
|
||||
expiresIn: 7d
|
||||
notBefore: 0
|
|
@ -0,0 +1,3 @@
|
|||
security:
|
||||
api:
|
||||
legacy: false
|
|
@ -0,0 +1,3 @@
|
|||
security:
|
||||
api:
|
||||
legacy: true
|
|
@ -0,0 +1,9 @@
|
|||
security:
|
||||
api:
|
||||
legacy: false
|
||||
sign:
|
||||
expiresIn: 7d
|
||||
notBefore: 0
|
||||
web:
|
||||
sign:
|
||||
expiresIn: 7d
|
1
test/unit/partials/store/storage/.sinopia-db.json
Normal file
1
test/unit/partials/store/storage/.sinopia-db.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"list":[],"secret":"c884521349204fdb6665e27a20c04dd1fb81863a56fa68cbbab39aebea0dd609"}
|
|
@ -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
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in a new issue