diff --git a/package.json b/package.json index c6d4393ca..16f4a6b7d 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "handlebars": "^4.0.5", "highlight.js": "^9.3.0", "http-errors": "^1.4.0", - "jju": "^1.3.0", "js-string-escape": "1.0.1", "js-yaml": "^3.6.0", + "jsonwebtoken": "^7.4.1", "lockfile": "^1.0.1", "lodash": "4.17.4", "lunr": "^0.7.0", diff --git a/src/api/web/api.js b/src/api/web/api.js index 8ea9d8e47..28a7dc313 100644 --- a/src/api/web/api.js +++ b/src/api/web/api.js @@ -1,7 +1,6 @@ 'use strict'; const bodyParser = require('body-parser'); -const Cookies = require('cookies'); const express = require('express'); const marked = require('marked'); const Search = require('../../lib/search'); @@ -12,6 +11,7 @@ const validatePkg = Middleware.validate_package; const securityIframe = Middleware.securityIframe; const route = express.Router(); // eslint-disable-line const async = require('async'); +const HTTPError = require('http-errors'); /* This file include all verdaccio only API(Web UI), for npm API please see ../endpoint/ @@ -28,9 +28,8 @@ module.exports = function(config, auth, storage) { route.param('version', validateName); route.param('anything', match(/.*/)); - route.use(Cookies.express()); route.use(bodyParser.urlencoded({extended: false})); - route.use(auth.cookie_middleware()); + route.use(auth.jwtMiddleware()); route.use(securityIframe); // Get list of all visible package @@ -112,13 +111,16 @@ module.exports = function(config, auth, storage) { } }); - route.post('/-/login', function(req, res, next) { + route.post('/login', function(req, res, next) { auth.authenticate(req.body.user, req.body.pass, (err, user) => { if (!err) { req.remote_user = user; - let str = req.body.user + ':' + req.body.pass; - res.cookies.set('token', auth.aes_encrypt(str).toString('base64')); + next({ + token: auth.issue_token(user, '24h'), + }); + } else { + next(HTTPError[err.message ? 401 : 500](err.message)); } let base = Utils.combineBaseUrl(Utils.getWebProtocol(req), req.get('host'), config.url_prefix); @@ -132,5 +134,10 @@ module.exports = function(config, auth, storage) { res.redirect(base); }); + // What are you looking for? logout? client side will remove token when user click logout, + // or it will auto expire after 24 hours. + // This token is different with the token send to npm client. + // 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/index.js b/src/api/web/index.js index 8e25a720d..6684bf742 100644 --- a/src/api/web/index.js +++ b/src/api/web/index.js @@ -1,7 +1,6 @@ 'use strict'; const async = require('async'); -const Cookies = require('cookies'); const escape = require('js-string-escape'); const express = require('express'); const fs = require('fs'); @@ -18,9 +17,7 @@ const env = require('../../config/env'); module.exports = function(config, auth, storage) { Search.configureStorage(storage); - - router.use(Cookies.express()); - router.use(auth.cookie_middleware()); + router.use(auth.jwtMiddleware()); router.use(securityIframe); Handlebars.registerPartial('entry', fs.readFileSync(require.resolve('../../webui/src/entry.hbs'), 'utf8')); diff --git a/src/lib/auth.js b/src/lib/auth.js index 8d97a3d59..2e97fd2fe 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -4,11 +4,11 @@ 'use strict'; const Crypto = require('crypto'); -const jju = require('jju'); const Error = require('http-errors'); const Logger = require('./logger'); const load_plugins = require('./plugin-loader').load_plugins; const pkgJson = require('../../package.json'); +const jwt = require('jsonwebtoken'); /** * Handles the authentification, load auth plugins. */ @@ -317,98 +317,71 @@ class Auth { } /** - * Set up cookie middleware. + * JWT middleware for WebUI * @return {Function} */ - cookie_middleware() { - let self = this; - return function(req, res, _next) { + jwtMiddleware() { + return (req, res, _next) => { + if (req.remote_user !== null && req.remote_user.name !== undefined) return _next(); + req.pause(); const next = function(_err) { req.resume(); return _next(); }; - if (req.remote_user != null && req.remote_user.name !== undefined) - return next(); - req.remote_user = buildAnonymousUser(); - let token = req.cookies.get('token'); - if (token == null) { - return next(); - } - let credentials = self.aes_decrypt(new Buffer(token, 'base64')).toString('utf8'); - if (!credentials) { - return next(); + let token = (req.headers.authorization || '').replace('Bearer ', ''); + if (!token) return next(); + + let decoded; + try { + decoded = this.decode_token(token); + } catch (err) {/**/} + + if (decoded) { + req.remote_user = authenticatedUser(decoded.user, decoded.group); } - let index = credentials.indexOf(':'); - if (index < 0) { - return next(); - } - const user = credentials.slice(0, index); - const pass = credentials.slice(index + 1); - - self.authenticate(user, pass, function(err, user) { - if (!err) { - req.remote_user = user; - next(); - } else { - req.remote_user = buildAnonymousUser(); - next(err); - } - }); + next(); }; } /** * Generates the token. - * @param {*} user - * @return {String} + * @param {object} user + * @param {string} expire_time + * @return {string} */ - issue_token(user) { - let data = jju.stringify({ - u: user.name, - g: user.real_groups && user.real_groups.length ? user.real_groups : undefined, - t: ~~(Date.now()/1000), - }, {indent: false}); - - data = new Buffer(data, 'utf8'); - const mac = Crypto.createHmac('sha256', this.secret).update(data).digest(); - return Buffer.concat([data, mac]).toString('base64'); + issue_token(user, expire_time) { + return jwt.sign( + { + user: user.name, + group: user.real_groups && user.real_groups.length ? user.real_groups : undefined, + }, + this.secret, + { + notBefore: '1000', // Make sure the time will not rollback :) + expiresIn: expire_time || '7d', + } + ); } /** * Decodes the token. - * @param {*} str - * @param {*} expire_time + * @param {*} token * @return {Object} */ - decode_token(str, expire_time) { - const buf = new Buffer(str, 'base64'); - if (buf.length <= 32) { - throw Error[401]('invalid token'); + decode_token(token) { + let decoded; + try { + decoded = jwt.verify(token, this.secret); + } catch (err) { + throw Error[401](err.message); } - let data = buf.slice(0, buf.length - 32); - let their_mac = buf.slice(buf.length - 32); - let good_mac = Crypto.createHmac('sha256', this.secret).update(data).digest(); - - their_mac = Crypto.createHash('sha512').update(their_mac).digest('hex'); - good_mac = Crypto.createHash('sha512').update(good_mac).digest('hex'); - if (their_mac !== good_mac) throw Error[401]('bad signature'); - - // make token expire in 24 hours - // TODO: make configurable? - expire_time = expire_time || 24*60*60; - - data = jju.parse(data.toString('utf8')); - if (Math.abs(data.t - ~~(Date.now()/1000)) > expire_time) { - throw Error[401]('token expired'); - } - - return data; + return decoded; } /**