diff --git a/.gitignore b/.gitignore
index 2cf001334..847f397e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/conf/default.yaml b/conf/default.yaml
index cacde36b0..b3723f6a6 100644
--- a/conf/default.yaml
+++ b/conf/default.yaml
@@ -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:
diff --git a/conf/docker.yaml b/conf/docker.yaml
index 4e75ef2b3..58bd434f2 100644
--- a/conf/docker.yaml
+++ b/conf/docker.yaml
@@ -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:
diff --git a/docs/config.md b/docs/config.md
index 666e24559..218c5f9d1 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -61,6 +61,31 @@ auth:
max_users: 1000
```
+### Security
+
+Since: `verdaccio@4.0.0` due [#168](https://github.com/verdaccio/verdaccio/pull/168)
+
+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).
diff --git a/docs/uplinks.md b/docs/uplinks.md
index c0a723bd6..dd3118542 100644
--- a/docs/uplinks.md
+++ b/docs/uplinks.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.
diff --git a/package.json b/package.json
index 8abb0398a..f242ecfb9 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -194,7 +194,8 @@
},
"lint-staged": {
"*.yaml": [
- "prettier --parser yaml --no-config --single-quote --write"
+ "prettier --parser yaml --no-config --single-quote --write",
+ "git add"
],
"*.js": [
"eslint .",
diff --git a/src/api/endpoint/api/user.js b/src/api/endpoint/api/user.js
index d68f67b23..3685029db 100644
--- a/src/api/endpoint/api/user.js
+++ b/src/api/endpoint/api/user.js
@@ -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',
diff --git a/src/api/endpoint/index.js b/src/api/endpoint/index.js
index 2275130e9..b489acec4 100644
--- a/src/api/endpoint/index.js
+++ b/src/api/endpoint/index.js
@@ -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);
diff --git a/src/api/index.js b/src/api/index.js
index f1d95b2d4..ab356484b 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -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));
diff --git a/src/api/web/api.js b/src/api/web/api.js
index c04255261..e447b8fff 100644
--- a/src/api/web/api.js
+++ b/src/api/web/api.js
@@ -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;
-};
+}
diff --git a/src/api/web/endpoint/user.js b/src/api/web/endpoint/user.js
index 8186de869..ac3f5f6a8 100644
--- a/src/api/web/endpoint/user.js
+++ b/src/api/web/endpoint/user.js
@@ -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));
}
});
});
diff --git a/src/api/web/index.js b/src/api/web/index.js
index 1124ecb56..0bb4f887e 100644
--- a/src/api/web/index.js
+++ b/src/api/web/index.js
@@ -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);
diff --git a/src/lib/auth-utils.js b/src/lib/auth-utils.js
index aa3586cf1..4e467cdfc 100644
--- a/src/lib/auth-utils.js
+++ b/src/lib/auth-utils.js
@@ -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): 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 {
+ 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);
+ }
+ }
+}
diff --git a/src/lib/auth.js b/src/lib/auth.js
index 9fc97a815..0d6cb1e7d 100644
--- a/src/lib/auth.js
+++ b/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;
- 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) {
- 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) {
- 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;
diff --git a/src/lib/config.js b/src/lib/config.js
index a01ea96f1..f5a8840e3 100644
--- a/src/lib/config.js
+++ b/src/lib/config.js
@@ -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;
diff --git a/src/lib/constants.js b/src/lib/constants.js
index a6bd09b3e..6a3ce3389 100644
--- a/src/lib/constants.js
+++ b/src/lib/constants.js
@@ -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 = {
diff --git a/src/lib/crypto-utils.js b/src/lib/crypto-utils.js
index 8c4c376ad..c0df6a1d3 100644
--- a/src/lib/crypto-utils.js
+++ b/src/lib/crypto-utils.js
@@ -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: '1000', // Make sure the time will not rollback :)
- ...options,
+export async function signPayload(
+ payload: RemoteUser,
+ secretOrPrivateKey: string,
+ options: JWTSignOptions): Promise {
+ 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);
}
diff --git a/src/lib/utils.js b/src/lib/utils.js
index 6c4da641d..2af8d30a4 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -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');
}
diff --git a/src/webui/utils/api.js b/src/webui/utils/api.js
index fa0436ca4..a48067bde 100644
--- a/src/webui/utils/api.js
+++ b/src/webui/utils/api.js
@@ -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))) {
diff --git a/test/functional/uplinks/cache.js b/test/functional/uplinks/cache.js
index 40600e060..cf36e7f14 100644
--- a/test/functional/uplinks/cache.js
+++ b/test/functional/uplinks/cache.js
@@ -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)
diff --git a/test/lib/request.js b/test/lib/request.js
index 5cbfb8bc3..41e81a0e9 100644
--- a/test/lib/request.js
+++ b/test/lib/request.js
@@ -116,7 +116,6 @@ function smartRequest(options: any): Promise {
// store the response on symbol
smartObject[requestData].response = res;
- // console.log("======>smartRequest RESPONSE: ", body);
resolve(body);
});
});
diff --git a/test/lib/server_process.js b/test/lib/server_process.js
index 5026f1562..7b7b8122d 100644
--- a/test/lib/server_process.js
+++ b/test/lib/server_process.js
@@ -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 {
diff --git a/test/unit/__helper.js b/test/unit/__helper.js
new file mode 100644
index 000000000..95b90b8c7
--- /dev/null
+++ b/test/unit/__helper.js
@@ -0,0 +1,5 @@
+import path from 'path';
+
+export const parseConfigurationFile = (name) => {
+ return path.join(__dirname, `./partials/config/yaml/${name}.yaml`);
+};
diff --git a/test/unit/api/__api-helper.js b/test/unit/api/__api-helper.js
new file mode 100644
index 000000000..d6af9f8fa
--- /dev/null
+++ b/test/unit/api/__api-helper.js
@@ -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]);
+ });
+ });
+}
diff --git a/test/unit/api/api.jwt.spec.js b/test/unit/api/api.jwt.spec.js
new file mode 100644
index 000000000..cdd3c76a2
--- /dev/null
+++ b/test/unit/api/api.jwt.spec.js
@@ -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();
+ });
+
+});
diff --git a/test/unit/api/auth-utils.spec.js b/test/unit/api/auth-utils.spec.js
new file mode 100644
index 000000000..934adb5c9
--- /dev/null
+++ b/test/unit/api/auth-utils.spec.js
@@ -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 {
+ 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([]);
+ });
+ });
+ });
+});
diff --git a/test/unit/api/auth.spec.js b/test/unit/api/auth.spec.js
index cdc68d896..0c88c9de1 100644
--- a/test/unit/api/auth.spec.js
+++ b/test/unit/api/auth.spec.js
@@ -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);
+ }
+ });
+ })
});
diff --git a/test/unit/api/config-utils.spec.js b/test/unit/api/config-utils.spec.js
index 667db85b5..f94bb794c 100644
--- a/test/unit/api/config-utils.spec.js
+++ b/test/unit/api/config-utils.spec.js
@@ -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();
diff --git a/test/unit/partials/config/yaml/api-jwt/jwt.yaml b/test/unit/partials/config/yaml/api-jwt/jwt.yaml
new file mode 100644
index 000000000..69bcfd118
--- /dev/null
+++ b/test/unit/partials/config/yaml/api-jwt/jwt.yaml
@@ -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}
diff --git a/test/unit/partials/config/yaml/security/security-basic.yaml b/test/unit/partials/config/yaml/security/security-basic.yaml
new file mode 100644
index 000000000..11679dbc1
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-basic.yaml
@@ -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
diff --git a/test/unit/partials/config/yaml/security/security-empty.yaml b/test/unit/partials/config/yaml/security/security-empty.yaml
new file mode 100644
index 000000000..9a4b4c9f7
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-empty.yaml
@@ -0,0 +1 @@
+security:
diff --git a/test/unit/partials/config/yaml/security/security-jwt-legacy-enabled.yaml b/test/unit/partials/config/yaml/security/security-jwt-legacy-enabled.yaml
new file mode 100644
index 000000000..a6d13bb79
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-jwt-legacy-enabled.yaml
@@ -0,0 +1,10 @@
+security:
+ api:
+ legacy: true
+ jwt:
+ sign:
+ expiresIn: 7d
+ notBefore: 0
+ web:
+ sign:
+ expiresIn: 7d
diff --git a/test/unit/partials/config/yaml/security/security-jwt.yaml b/test/unit/partials/config/yaml/security/security-jwt.yaml
new file mode 100644
index 000000000..075a85201
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-jwt.yaml
@@ -0,0 +1,6 @@
+security:
+ api:
+ jwt:
+ sign:
+ expiresIn: 7d
+ notBefore: 0
diff --git a/test/unit/partials/config/yaml/security/security-legacy-disabled.yaml b/test/unit/partials/config/yaml/security/security-legacy-disabled.yaml
new file mode 100644
index 000000000..3ed6e7792
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-legacy-disabled.yaml
@@ -0,0 +1,3 @@
+security:
+ api:
+ legacy: false
diff --git a/test/unit/partials/config/yaml/security/security-legacy.yaml b/test/unit/partials/config/yaml/security/security-legacy.yaml
new file mode 100644
index 000000000..6602c9daf
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-legacy.yaml
@@ -0,0 +1,3 @@
+security:
+ api:
+ legacy: true
diff --git a/test/unit/partials/config/yaml/security/security-missing.yaml b/test/unit/partials/config/yaml/security/security-missing.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/unit/partials/config/yaml/security/security-no-legacy.yaml b/test/unit/partials/config/yaml/security/security-no-legacy.yaml
new file mode 100644
index 000000000..dba590e79
--- /dev/null
+++ b/test/unit/partials/config/yaml/security/security-no-legacy.yaml
@@ -0,0 +1,9 @@
+security:
+ api:
+ legacy: false
+ sign:
+ expiresIn: 7d
+ notBefore: 0
+ web:
+ sign:
+ expiresIn: 7d
diff --git a/test/unit/partials/store/storage/.sinopia-db.json b/test/unit/partials/store/storage/.sinopia-db.json
new file mode 100644
index 000000000..769c4f86f
--- /dev/null
+++ b/test/unit/partials/store/storage/.sinopia-db.json
@@ -0,0 +1 @@
+{"list":[],"secret":"c884521349204fdb6665e27a20c04dd1fb81863a56fa68cbbab39aebea0dd609"}
\ No newline at end of file
diff --git a/types/index.js b/types/index.js
index e0e6eba2c..7dd745e90 100644
--- a/types/index.js
+++ b/types/index.js
@@ -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 {
diff --git a/yarn.lock b/yarn.lock
index a6b814599..603bec0ef 100644
Binary files a/yarn.lock and b/yarn.lock differ