Skip to content

Commit

Permalink
feat: add support for jwt on api
Browse files Browse the repository at this point in the history
  • Loading branch information
juanpicado committed Aug 5, 2018
1 parent 51c59f8 commit ac1936f
Show file tree
Hide file tree
Showing 14 changed files with 168 additions and 68 deletions.
13 changes: 13 additions & 0 deletions conf/full.yaml
Expand Up @@ -22,6 +22,19 @@ auth:
# You can set this to -1 to disable registration.
#max_users: 1000

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

# Configure plugins that can register custom middlewares
# To use `npm audit` uncomment the following section
middlewares:
Expand Down
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -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",
Expand All @@ -53,7 +53,7 @@
"devDependencies": {
"@commitlint/cli": "7.0.0",
"@commitlint/config-conventional": "7.0.1",
"@verdaccio/types": "3.4.2",
"@verdaccio/types": "3.7.0",
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-eslint": "8.2.6",
Expand All @@ -77,8 +77,8 @@
"babel-register": "6.26.0",
"babel-runtime": "6.26.0",
"bundlesize": "0.17.0",
"cross-env": "5.1.4",
"codecov": "3.0.4",
"cross-env": "5.1.4",
"css-loader": "0.28.10",
"element-react": "1.4.8",
"element-theme-default": "1.4.13",
Expand Down
46 changes: 24 additions & 22 deletions src/api/endpoint/api/user.js
@@ -1,34 +1,38 @@
// @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';

import {ErrorCode} from '../../../lib/utils';
import {API_MESSAGE, HTTP_STATUS} from '../../../lib/constants';
import {buildUserBuffer, createSessionToken, getAuthenticatedMessage} from '../../../lib/auth-utils';

import type {$Response, Router} from 'express';
import type {$RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth} from '../../../../types';

export default function(route: Router, auth: IAuth) {
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;
const {name, password} = req.body;
const token = (name && password) ? auth.aesEncrypt(buildUserBuffer(name, password)).toString('base64') : undefined;

if (_.isNil(req.remote_user.name) === false) {
res.status(201);
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, 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.
Expand All @@ -38,30 +42,28 @@ export default function(route: Router, auth: IAuth) {
}

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,
});
});


// 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',
Expand Down
6 changes: 4 additions & 2 deletions src/api/index.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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));
Expand Down
4 changes: 2 additions & 2 deletions src/api/web/api.js
Expand Up @@ -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
Expand All @@ -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;
};
}
21 changes: 14 additions & 7 deletions 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, (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: auth.issueUIjwt(user, jWTSignOptions),
username: req.remote_user.name,
});
} else {
next(HTTPError[err.message ? 401 : 500](err.message));
}
});
});
Expand Down
54 changes: 51 additions & 3 deletions src/lib/auth-utils.js
@@ -1,9 +1,17 @@
// @flow

import _ from 'lodash';
import {ErrorCode} from './utils';
import {API_ERROR} from './constants';
import {API_ERROR, TIME_EXPIRATION_7D} from './constants';

import type {RemoteUser, Package, Callback} from '@verdaccio/types';
import type {
RemoteUser,
Package,
Callback,
Config,
Security,
APITokenOptions,
JWTOptions} from '@verdaccio/types';
import type {CookieSessionToken} from '../../types';

export function allow_action(action: string) {
return function(user: RemoteUser, pkg: Package, callback: Callback) {
Expand Down Expand Up @@ -36,3 +44,43 @@ export function getDefaultPlugins() {
allow_publish: allow_action('publish'),
};
}

export function createSessionToken(): CookieSessionToken {
return {
// npmjs.org sets 10h expire
expires: new Date(Date.now() + 10 * 60 * 60 * 1000),
};
}

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 new Buffer(`${name}:${password}`);
}
13 changes: 6 additions & 7 deletions src/lib/auth.js
Expand Up @@ -10,11 +10,11 @@ import {getDefaultPlugins} from './auth-utils';

import {getMatchedPackagesSpec} from './config-utils';

import type {Config, Logger, Callback, IPluginAuth, RemoteUser} from '@verdaccio/types';
import type {
Config, Logger, Callback, IPluginAuth, RemoteUser, JWTSignOptions,
} from '@verdaccio/types';
import type {$Response, NextFunction} from 'express';
import type {$RequestExtend, JWTPayload} from '../../types';
import type {IAuth} from '../../types';

import type {$RequestExtend, JWTPayload, IAuth} from '../../types';

const LoggerApi = require('./logger');

Expand All @@ -23,7 +23,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;
Expand Down Expand Up @@ -283,14 +282,14 @@ class Auth implements IAuth {
};
}

issueUIjwt(user: any, expiresIn: string) {
issueUIjwt(user: RemoteUser, signOptions: JWTSignOptions) {
const {name, real_groups} = user;
const payload: JWTPayload = {
user: name,
group: real_groups && real_groups.length ? real_groups : undefined,
};

return signPayload(payload, this.secret, {expiresIn: expiresIn || Auth.DEFAULT_EXPIRE_WEB_TOKEN});
return signPayload(payload, this.secret, signOptions);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/lib/config.js
Expand Up @@ -15,6 +15,7 @@ import {APP_ERROR} from './constants';
import type {
PackageList,
Config as AppConfig,
Security,
Logger,
} from '@verdaccio/types';

Expand All @@ -38,6 +39,7 @@ class Config implements AppConfig {
self_path: string;
storage: string | void;
plugins: string | void;
security: Security;
$key: any;
$value: any;

Expand Down
7 changes: 5 additions & 2 deletions 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',
Expand Down Expand Up @@ -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 = {
Expand Down
22 changes: 15 additions & 7 deletions src/lib/crypto-utils.js
@@ -1,10 +1,18 @@
// @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} from '@verdaccio/types';
import type {JWTPayload} from '../../types';

export const defaultAlgorithm = 'aes192';
export const defaultTarballHashAlgorithm = 'sha1';

export function aesEncrypt(buf: Buffer, secret: string): Buffer {
const c = createCipher(defaultAlgorithm, secret);
Expand All @@ -26,7 +34,7 @@ export function aesDecrypt(buf: Buffer, secret: string) {
}

export function createTarballHash() {
return createHash('sha1');
return createHash(defaultTarballHashAlgorithm);
}

/**
Expand All @@ -44,13 +52,13 @@ 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, {
export function signPayload(payload: JWTPayload, secretOrPrivateKey: string, options: JWTSignOptions) {
return jwt.sign(payload, secretOrPrivateKey, {
notBefore: '1000', // Make sure the time will not rollback :)
...options,
});
}

export function verifyPayload(token: string, secret: string) {
return jwt.verify(token, secret);
export function verifyPayload(token: string, secretOrPrivateKey: string) {
return jwt.verify(token, secretOrPrivateKey);
}

0 comments on commit ac1936f

Please sign in to comment.